Scroll Transformation Story — Free GSAP ScrollTrigger Before/After Wipe Reveal

Scroll Transformation Story · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Scrubbed clip-path wipe
Scroll position directly controls how much of the "after" state is revealed.
No blended crossfade
clip-path shows a hard, literal split instead of an ambiguous opacity blend.
Single source of truth
One progress value drives the wipe, divider position, and progress bar together.
Threshold-based captions
Discrete narrative text changes layered on top of continuous scrub progress.
Fully bidirectional
Scrolling up retreats the wipe smoothly, frame for frame.
Content-agnostic wipe mechanic
Works for code, screenshots, copy, or any equal-size before/after pair.
Glowing divider indicator
A visible edge line always sits exactly where the wipe currently is.
Single pinned section
No separate scroll distance bookkeeping beyond one ScrollTrigger.

About this UI Snippet

Scroll Transformation Story — A Before/After Wipe Driven by Scroll Position

Screenshot of the Scroll Transformation Story snippet rendered live

A drag-to-compare slider needs a visitor to actively operate it. This snippet instead ties the same "before becomes after" wipe directly to scroll position, so the transformation narrates itself as part of reading the page — scroll down and the rewrite visibly overtakes the legacy code; scroll back up and it retreats, exactly in step.

`clip-path: inset()` as a scrubbed wipe, not a crossfade

The "after" panel sits stacked exactly on top of the "before" panel and starts with clip-path: inset(0 100% 0 0) — fully clipped away from its right edge. A single ScrollTrigger with scrub: 0.4 writes a new inset(0 N% 0 0) value every scroll update, where N shrinks from 100 to 0 as self.progress climbs from 0 to 1. Unlike an opacity crossfade, a clip-path wipe never shows both versions blended together mid-transition — at any given scroll position, the frame shows a hard, honest split between exactly how much "before" and how much "after" is visible, which reads far more clearly as a literal transformation in progress.

One progress value drives three synchronized elements

The same self.progress inside onUpdate sets the clip-path percentage, positions a glowing divider line at the identical edge (divider.style.left = pct + '%'), and fills a progress bar — three visibly different UI pieces, one shared number, so nothing can drift out of alignment the way separately-triggered animations might.

Threshold-based captions, not per-frame text

Captions live in a small array of { at, text } pairs and are resolved each update by finding the highest at value the current progress has passed — a lightweight step function layered on top of a continuous scrub, giving discrete narrative beats ("the rewrite begins to take over...") without needing a second ScrollTrigger or a character-by-character tween.

Why `scrub` instead of a triggered one-shot wipe

Using scrub (rather than a played-once tween triggered on enter) makes the wipe bidirectional and exploratory by nature — a visitor can scroll slowly and watch the exact code line where the transformation crosses, or scroll back up to compare a specific point again, which a fire-once reveal can't offer.

Customizing it

Swap the two <pre> code blocks for any other before/after content — a UI screenshot pair, a paragraph of edited copy, a data table — the wipe mechanic only cares that both panels are the same size and stacked. For a visitor-controlled (rather than scroll-controlled) version of the same clip-path idea, see scroll before/after; this snippet is the narrative, hands-off counterpart.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why a clip-path wipe communicates a before/after transformation more clearly than an opacity crossfade, and how driving the wipe percentage, the divider line's position, and the progress bar from the same single self.progress value inside one onUpdate callback keeps all three permanently in sync. The same assistant can help extend the pattern — ask it to add a second wipe axis (vertical instead of horizontal) for a different visual metaphor, layer in a subtle particle or glow effect right at the divider line, or add syntax-highlighting to the code panels using a highlighting library loaded alongside GSAP. Treat the code as a working starting point for your own scroll-driven transformation narrative.

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 transformation story" in plain HTML, CSS, and JavaScript using GSAP with its ScrollTrigger plugin, loaded from a CDN with no bundler.

Requirements:
- A pinned section containing two same-size panels stacked exactly on top of each other (position: absolute, inset: 0) representing a "before" state and an "after" state — any equal-size content works, such as two code blocks, two screenshots, or two paragraphs of text — plus a thin divider line element and a caption text element, preceded by an intro section and followed by an outro section.
- Give the "after" panel an initial clip-path of inset(0 100% 0 0), fully hiding it from its right edge, so only the "before" panel underneath is visible at the very start.
- Create a single ScrollTrigger on the pinned section with scrub enabled (a fractional value for slight easing lag), and inside its onUpdate callback, compute the clip-path's right-inset percentage as 100 minus the current scroll progress percentage, so the "after" panel's clip-path right inset shrinks from 100% to 0% in exact proportion to how far the visitor has scrolled through the pinned section — creating a hard-edged horizontal wipe rather than an opacity blend.
- In that same onUpdate callback, position a visible divider line's left offset to match the exact same progress percentage used for the clip-path, and update a separate progress bar fill's width from the same percentage — all three should be derived from one shared progress value so they can never fall out of sync with each other.
- Also in the same onUpdate callback, update a caption text element by checking the current scroll progress against a small ordered array of threshold-and-text pairs, displaying whichever caption's threshold is the highest one the current progress has already reached or passed — giving discrete narrative text changes layered on top of the continuous scrub-driven wipe.
- Ensure scrolling back up smoothly reverses the wipe, the divider position, the progress bar, and the captions, entirely through the scrub-driven ScrollTrigger with no separate reverse-direction logic required.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Add GSAP and ScrollTriggerLoad both from the CDN and call gsap.registerPlugin(ScrollTrigger).
  2. 2
    Paste the HTML, CSS, and JSThe "after" panel starts fully clipped away behind the "before" panel.
  3. 3
    Scroll into the pinned sectionA clip-path wipe reveals the "after" state in exact step with scroll progress.
  4. 4
    Watch the divider and captionsThe glowing line and caption text update from the same progress value.
  5. 5
    Scroll back upThe wipe retreats smoothly, since scrub ties it directly to scroll position.
  6. 6
    Swap in your own contentReplace the two code panels with any equal-size before/after pair.

Real-world uses

Common Use Cases

Code refactor / rewrite case studies
Show legacy code being visibly replaced by a cleaner rewrite as the reader scrolls.
Design or UI redesign showcases
Wipe from an old interface screenshot to the redesigned one.
Copy editing / content transformation demos
Reveal an edited, tightened version of a paragraph overtaking the original.
Home renovation or product transformation stories
Narrate a physical before/after with the same scroll-driven wipe.
Data cleanup or migration case studies
Show a messy dataset transforming into a cleaned, structured one.
Brand refresh microsites
Wipe from an old logo/identity system to the new one as part of the story.

Got questions?

Frequently Asked Questions

Animating opacity would blend both versions together at every point mid-transition, which reads ambiguously — the reader can't tell exactly how much has changed at any given scroll position. clip-path: inset() instead shows a hard edge: everything left of the divider is definitively "after" and everything right of it is definitively "before," with no blending, which communicates a literal, honest transformation rather than a soft fade.

All three are derived from the exact same self.progress value on every scroll update, rather than each having its own separate trigger or calculation. That guarantees they can never visually drift apart from each other — the divider line is mathematically guaranteed to sit exactly on the wipe's actual edge, because both are computed from the same number in the same function call.

The captions array stores threshold points (the at value, from 0 to 1) alongside their text, and every onUpdate call loops through the array to find the highest threshold the current progress has already passed. This is a simple step function layered on top of the single continuous scrub value — no additional ScrollTrigger instances or timeline segments are needed to get discrete text changes at specific scroll points.

Yes — the mechanic only requires that the "before" and "after" panels are the same size and stacked in the same position, which the CSS already sets up with position: absolute; inset: 0. Replace the <pre> code blocks with <img> tags, screenshots, or any other content of matching dimensions and the same clip-path scrub logic keeps working unchanged.

Create the ScrollTrigger inside a mount effect (useEffect, onMounted, or ngAfterViewInit) after both panels have rendered, and write the clip-path, divider position, and caption text as direct style/DOM updates inside onUpdate rather than through component state, since state updates at scroll-frame frequency would cause excessive re-renders. Call .kill() on the ScrollTrigger instance in the cleanup function.