Scroll Before After — Free GSAP Image Wipe Snippet

Scroll Before After · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Scroll-driven wipe
The scrollbar sweeps the comparison divider.
No-squish clipping
Fixed-width inner scene inside a widening window.
Single proxy value
One tweened number drives window and divider.
Glowing seam
Divider with handle sits exactly on the edge.
Pinned + scrubbed
160% of scroll maps to the full reveal.
Bidirectional
Scroll up to restore the before view.
CSS placeholder scenes
Gradient scenes swap cleanly for photos.
Anchored labels
After tag reveals with its own layer.

About this UI Snippet

Scroll Before After — A Comparison Wipe Driven by the Scrollbar

Screenshot of the Scroll Before After snippet rendered live

The scroll before/after is an image-comparison slider with the drag handle removed: the page's own scrollbar sweeps the divider. As you scroll through the pinned section, the "after" scene wipes across the "before" scene from left to right, and scrolling back restores it. It's a strong pattern for redesigns, photo edits, and renovation reveals because the user controls the reveal speed with a gesture they were already making. This snippet builds it with GSAP ScrollTrigger (from a CDN) and a two-layer clipping trick.

The wipe is a widening window, not a moving image

Both scenes are absolutely stacked in the frame. The .sba-after layer starts at width: 0% with overflow: hidden, while its inner .sba-scene is locked to the full frame width. Widening the outer layer therefore *uncovers* more of a stationary scene rather than stretching it — the classic comparison-slider clipping trick. Without the fixed-width inner element, the after image would squish horizontally as the window grew, which is the number-one bug in naive implementations.

One proxy tween drives both the window and the divider

GSAP tweens a plain { p: 0 } object to 100 on a pinned, scrubbed ScrollTrigger. The onUpdate callback writes the same percentage to two places: the after layer's width and the divider's left. Sharing one number guarantees the glowing seam always sits exactly on the reveal edge — there's no second animation that could drift out of phase.

Pinned scrub makes the reveal deliberate and reversible

start: 'top top' with end: '+=160%' pins the frame for 1.6 viewport-heights of scrolling, mapping the full 0–100% wipe onto that distance. scrub: 0.3 adds just enough smoothing that wheel ticks glide rather than jump. Because scrub ties progress to scroll position rather than playing a one-shot animation, the comparison is bidirectional — users naturally rock back and forth across the seam to compare details, which is exactly how people use drag-handle sliders.

The scenes are pure CSS, ready to swap for images

Each scene is a sky gradient, a rounded hill, and a sun — deliberately simple placeholders that make the wipe legible in the demo. In production you'd replace each .sba-scene with an <img> (or background-image) at the same fixed width; the clipping math is identical. Matching camera position between the two shots is what sells the effect.

Why width instead of clip-path

Animating clip-path: inset() also works, but a width-based window keeps the divider trivial to position (same percentage) and works in every browser that runs GSAP, including ones with patchy clip-path transition support. The layers are compositor-friendly because nothing outside the frame reflows — the frame's size never changes.

Labels stay anchored to their own layers

The "Before" tag lives on the base layer's left corner and "After" pins to the right corner *inside* the clipped layer, so it only becomes visible once enough of the after scene is revealed — a free bit of progressive labeling that reinforces which side is which.

Scroll-driven beats drag-driven on touch screens

Classic comparison sliders bind a horizontal drag to the divider — which collides head-on with vertical page scrolling on touch devices: the browser has to guess whether a diagonal swipe means "scroll the page" or "move the handle," and either choice frustrates someone. Driving the wipe from vertical scroll removes the entire input layer: no pointer handlers, no touch-action negotiations, no clamping math, and the one gesture mobile users already perform controls the comparison. The handle in this snippet is purely decorative — a landmark for the seam, not a control — which is also why it needs no hit area or focus handling.

Customizing it

Swap in real screenshots or photos, flip the wipe direction by anchoring the after layer to the right, or shorten end for a snappier reveal. For a drag-controlled version see the image comparison snippet; pair this one with a scroll zoom hero opener or a scroll curtain reveal transition into the next section.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to reverse-engineer the clipping trick from scratch. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the after layer's inner scene is locked to the full frame width while the outer layer's width is what animates, and why writing the tweened value directly in onUpdate instead of through framework state is the deliberate choice here. The same assistant is useful for optimizing it — ask whether updating two separate style properties on every onUpdate tick could be consolidated, or whether a CSS custom property driven by one style write would reduce the per-tick work. It is just as useful for extending the effect — ask it to add a draggable handle on top of the scroll-driven wipe for hybrid control, support a vertical wipe direction instead of horizontal, or sync a caption that changes text once the wipe crosses 50%. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-driven before/after comparison wipe" in plain HTML, CSS, and JavaScript using GSAP and its ScrollTrigger plugin (load both from a CDN, no build step) — no drag handle, no pointer event handlers.

Requirements:
- Two scenes (before and after) stacked absolutely on top of each other inside a fixed-aspect-ratio frame, each scene's inner content locked to the full frame's pixel width so it never gets stretched or squished.
- The "after" layer must start at 0% width with overflow hidden, acting purely as a widening clipping window that uncovers more of its full-width inner scene as it grows — the inner scene itself must never change size, only the window around it.
- Drive the wipe with a single tweened proxy object (a plain JS object with one numeric property, not two separate tweens) animated from 0 to 100 on a pinned, scrubbed ScrollTrigger, and in that tween's onUpdate callback, write that same one percentage value to both the after layer's width style and a vertical divider element's left/position style, so the divider always sits exactly on the reveal edge with no possibility of drift between the two.
- Configure the ScrollTrigger with pin: true and a scrub value (not scrub: true with zero smoothing, and not an unscrubbed one-shot animation) so the wipe tracks scroll position with a slight easing lag, and make the effect fully bidirectional — scrolling back up must visibly restore the before scene.
- Do not use CSS clip-path for the reveal mechanism — the reveal must be implemented via an animated width on the overlay layer, not a clip-path inset or a mask.
- Style each scene's "before"/"after" label so it is anchored inside its own layer (for example the after-layer label pinned to the right edge of the clipped inner scene), so a label only becomes visible once its layer is sufficiently uncovered.

Step by step

How to Use

  1. 1
    Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSTwo stacked scenes with a zero-width after window.
  3. 3
    Scroll into the stageThe frame pins and the divider starts sweeping.
  4. 4
    Rock back and forthThe wipe is scrubbed — compare any region freely.
  5. 5
    Finish the wipeAt 100% the after scene fully replaces the before.
  6. 6
    Swap in real imagesReplace each scene with a fixed-width img or background.

Real-world uses

Common Use Cases

Redesign showcases
Wipe from old UI to new; follow with a scroll reveal grid of improvements.
Photo editing demos
Show raw versus graded shots; offer a drag version with image comparison.
Renovation reveals
Before/after property shots inside a scroll pin story.
Performance case studies
Old versus optimized dashboards, then a scroll story chart with the numbers.
Landing transitions
Use the wipe as a section handoff like a scroll curtain reveal.
Product upgrades
Contrast plans or tiers before a pricing card block.

Got questions?

Frequently Asked Questions

A pinned ScrollTrigger scrubs a proxy object from p: 0 to p: 100 across 160% of scroll distance. Its onUpdate writes that one percentage to both the after layer's width and the divider's left offset, so the seam always sits exactly on the reveal edge and sweeping is fully reversible.

The after layer is only a clipping window — overflow: hidden with an animated width — while the scene inside it is locked to the full frame width. Widening the window uncovers more of a stationary image instead of resizing it. Skipping that fixed-width inner element is the classic bug that makes comparison sliders stretch.

Replace each .sba-scene's contents with an img styled to the same fixed frame width (width: min(760px, 92vw); height: 100%; object-fit: cover). Keep both shots aligned — same crop and camera position — because the wipe only reads as a true comparison when features line up across the seam.

Yes. For right-to-left, anchor the after layer to the right (right: 0 instead of left) and pin its inner scene to the right edge, updating the divider from the right. For a vertical wipe, animate height with a fixed-height inner scene and move the divider's top. The single-proxy pattern stays identical.

Horizontal drag handles fight vertical page scrolling on touch — the browser must guess whether a diagonal swipe scrolls or drags, and touch-action tuning only partially fixes it. Scroll-driving the wipe eliminates the conflict: the gesture users already make controls the comparison, there are zero pointer handlers to write, and the divider becomes a decorative landmark that needs no hit area, clamping, or focus management.

Build the tween in a mount effect (useEffect, onMounted, or ngAfterViewInit) with refs for the stage, after layer, and divider, and revert the gsap.context in the cleanup so the pin unregisters on unmount. Writing styles directly in onUpdate is deliberate — routing the percentage through framework state would re-render every scroll tick. Tailwind handles the frame styling cleanly.