# Before / After Compare

> 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.

Canonical: https://gsapvault.com/effects/before-after-compare
Live demo: https://gsapvault.com/demos/before-after-compare/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | intermediate |
| Plugins | Core GSAP only |
| Techniques | velocity-reactive, before-after, clip-path, draggable, spring-physics, keyboard-navigation, click-toggle, touch-gestures, svg-path, elastic-divider |
| Uses Lenis | No |

## Lighthouse, as measured

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

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

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.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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` | `0`–`1` | `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` | `0`–`0.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:** <kbd>←</kbd>/<kbd>↓</kbd> and <kbd>→</kbd>/<kbd>↑</kbd> step 5%, <kbd>Home</kbd> and <kbd>End</kbd> jump to the ends, <kbd>Space</kbd> or <kbd>Enter</kbd> 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](https://unsplash.com/photos/a-living-room-with-a-green-couch-and-a-coffee-table-0JGRXomKXSw), available under the [Unsplash License](https://unsplash.com/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.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Before / After Compare](https://gsapvault.com/effects/before-after-compare)
- [The Vault (£99 one-time, best value): everything in the catalogue, including future releases](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Image Clip Reveal](https://gsapvault.com/effects/image-clip-reveal.md): A cinematic image reveal where a directional polygon aperture opens as the photograph settles from a restrained Ken Burns scale and its caption lands.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
