079

Before / After Compare

£5

A before-and-after reveal with a flexible image boundary. Drag the slim grip to bend the divider, flick to throw it, then watch the edge spring straight.

intermediate
10 more details
velocity-reactivebefore-afterclip-pathdraggablespring-physicskeyboard-navigationclick-toggletouch-gesturessvg-pathelastic-divider
Before / After Compare - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

A before-and-after image slider with an elastic divider. The grip follows your hand while the top and bottom of the image boundary trail behind, creating a flexible edge that springs straight when it settles. The photographs stay fixed, so the transformation remains clear.

Read the full effect overview

A slim ridged grip tilts with velocity and follows your hand vertically. Before, Split and After controls let visitors inspect the complete images, while touch and keyboard controls work alongside reduced-motion support.

What's included

9 items
  • Flexible SVG image boundary: the clip and visible seam share exactly the same curve
  • Independently sprung top and bottom edges trail the grip and settle precisely straight
  • Slim ridged grip follows your hand vertically, tilts with velocity and returns to centre
  • Sharp, stationary imagery with no default blur, skew or zoom
  • Flick momentum and a restrained spring at the frame edges
  • Optional Before, Split and After buttons with accessible pressed states
  • Keyboard arrows, Home, End and Space with live accessible slider values
  • Touch drag preserves vertical page scrolling; reduced motion uses an immediate straight divider
  • Independent instances and full teardown restore the original styles, markup and ARIA values

Perfect for

5 use cases
  • Photo retouching and colour grading portfolios showing the finished frame against the original
  • Cosmetic, dental and clinical clinics pairing treatment imagery with a compliant, non-graphic before state
  • Furniture, interiors and renovation studios showing a room before and after a fit-out
  • E-commerce product listings where a material, finish or scale difference needs proving
  • Anywhere a static two-up comparison would leave the reader unconvinced

How it works

2 sections

A single pointer position moves the grip, while two damped springs follow it at the top and bottom of the frame. Their positions define a cubic Bezier curve. The same curve draws the visible divider and clips the before image, keeping the flexible boundary aligned.

GSAP animates the throw and sweep. A time-based ticker advances the springs, follows the pointer vertically and tilts the grip according to its speed. Reduced motion skips the springs, tilt and inertia. Each instance removes its generated SVG, listeners and styles when the GSAP context is reverted.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, 9 September 2026. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Before / After Compare opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add before closing your </body> tag:

Code snippet omitted: it ships with the download.

3. Add the comparison to your <body>:

Code snippet omitted: it ships with the download.

Everything inside the frame is positional; the script only reads the five data-compare-* hooks and writes the position and handle-scale custom properties.

Using It With Your Own Design

What the effect needs from your markup. Five hooks, and nothing else:

Hook Where Required
data-compare the wrapper around the whole widget yes
data-compare-frame the element that owns the frame's box; also the focusable control yes
data-compare-before the plate that is revealed from the left yes
data-compare-after the plate underneath it yes
data-compare-handle the grip yes

The script never queries a class name, so you can rename every class in the snippet. When removing the demo shell, set --frame-w: 100%; --ar-w: 16; --ar-h: 10 on your .compare wrapper so the frame fills its own container.

The two <figure>s must be siblings inside the same clipping wrapper, and data-compare-before must come after data-compare-after in source order, because the before plate paints on top and is the one being clipped.

What is only the demo's styling. Almost all of style.css: the palette, the Mona Sans stack, the .showcase-* shell, the grip's size, the labels and the paper ground are the demo, not the effect. The only rules the mechanic depends on are listed next.

CSS the effect depends on:

  • The frame must not clip. .compare__plates is the overflow: clip layer, and the grip lives outside it, which is what lets a thrown handle leave the frame instead of vanishing at the border. Do not put overflow: hidden on the frame.
  • The before plate is clipped, not sized. The CSS inset clip supplies a straight no-JS fallback. The script inserts a unique SVG clip path and replaces the inline clip with its URL. Keep [data-compare-seam] absolutely positioned to fill the clipping wrapper. Its visible path and the image clip share the same curve. Hide the fallback .compare__edge only under [data-compare-live="true"].
  • The grip reads --p. Its horizontal position is left: calc(var(--p) * 1%); its vertical position is top: var(--grip-y, 50%). Preserve these and its --grip-tilt, --sx and --sy transforms. The SVG boundary meets the grip at that same position.
  • Keep the photographs registered. Both images fill the same frame with inset: 0; width: 100%; height: 100%; object-fit: cover. There is no image shear or zoom. Use the same crop and object position for both.
  • touch-action: pan-y on the frame. It is what keeps a vertical swipe scrolling the page instead of being swallowed by the comparison. Remove it and mobile readers cannot scroll past the widget.
  • pointer-events: none on the grip. The frame is the control, so a finger anywhere inside it starts a drag. Nothing is lost by making the grip decorative.

Options

Set these on the [data-compare] wrapper.

Attribute Values Default Description
data-compare-start 01 0.5 Resting position of the seam as a fraction of the frame width
data-compare-detents comma-separated fractions 0.25,0.5,0.75 Positions a slow release snaps to
data-compare-commit px per second 110 Release speed above which a gesture is a throw rather than a placement
data-compare-momentum seconds 0.34 How far a throw is carried forward before it settles
data-compare-snap px 24 How close a slow release has to be to a detent to snap
data-compare-blur px 0 Blur on the covered plate at full cover; 0 disables the focus pull
data-compare-overrun fraction of frame width 0.02 Furthest the grip may be shown past either end
data-compare-flex 00.25 0.11 Maximum curve deflection as a fraction of frame width; 0 keeps a straight edge
data-compare-sweep seconds 1.05 Duration of the click sweep

Choosing Your Images

This effect only shows two states of the same frame, so the difference between your two files is the demo. Two similar exports read as a broken widget, not a comparison.

  • Same subject, same framing, same crop. Tripod, same focal length, same distance. If the two frames are off by a few percent the viewer reads parallax, not change, and the seam looks like a mistake. Align your exports around unchanged details where possible.
  • Make the difference obvious at a glance. A grade difference, a repair, a fit-out, a finish, a lighting change: it has to register in the half-second before anyone touches the handle. Subtlety here reads as "nothing happened".
  • Export both at the same pixel dimensions and let object-fit: cover handle the rest. The demo uses 1800×1800 WebP images, with identical object positions in a 16:10 desktop frame and a 4:5 mobile frame. Change the aspect ratio to suit your own pair.
  • Small files matter more than perfect ones. This effect can hold two full-width photographs and a blur filter on one of them at the same time, so keep each plate under about 350 KB and test the drag on a mid-range phone. data-compare-blur="0" is the escape hatch if a low-power device struggles.
  • Do not bake text, watermarks or labels into the images. The demo labels sit outside the frame for that reason.

Replace assets/img/frame-before.webp and assets/img/frame-after.webp with your own exports, or edit assets/img-manifest.json and re-run bun scripts/build-template-assets.ts before-after-compare if you are working inside this repository.

Accessibility

  • One control, not two. The frame carries role="slider" with aria-valuemin, aria-valuemax, aria-valuenow and an aria-valuetext that reads "42% before, 58% after". The grip is decorative and hidden from assistive technology, so there is exactly one tab stop and one thing to operate.
  • Keyboard: / and / step 5%, Home and End jump to the ends, Space or Enter plays the sweep to the opposite side. Focus is drawn as a real ring in the accent colour.
  • Reduced motion: the widget opens at the configured start position, a drag places the seam exactly where it is released (no inertia, no detent snapping, no spring), keyboard steps and the sweep are applied instantly, and the flexible curve, vertical following, tilt, optional blur and grip stretch are all switched off. Dragging still works, because the comparison is the content.
  • Touch: vertical swipes scroll the page normally. A gesture that starts moving down the screen is handed straight back to the browser and changes nothing.
  • Without JavaScript: the CSS default holds the pair split at 50%, both states visible and both labels present.

Dependencies

Required:

  • GSAP 3.12+ (the demo pins 3.15.0)

Not required:

  • No plugins. The drag, the momentum, the rubber band and the spring are all core GSAP, and the script adds one callback to gsap.ticker.
  • No Lenis. The effect takes pointer input only and never scrolls, so it does not care how the host page scrolls.

Optional View Controls

Place these inside [data-compare], outside the slider frame. The script updates their aria-pressed state and includes their listeners in teardown. Omit them if the drag interaction is all you need.

Code snippet omitted: it ships with the download.

Demo Photography

This is an interior styling concept, not a documented renovation. The furnished image is Spacejoy's interior on Unsplash, available under the Unsplash License. The empty version was produced by AI-assisted furniture removal, preserving the camera and architecture as closely as possible. The demo caption identifies it as a concept.

The source and generation provenance are recorded in assets/img-manifest.json; the generated source is preserved in assets/img-src/empty-room.png. Both displayed images use identical sizing and object position. Replace them with your own corresponding exports for a real project.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

Everything in the catalogue, plus everything we release next.