Motion18 July 20268 min readNULLLAB

Scroll storytelling: pinning, scrubbing, and the discipline of not overdoing it

Tying a timeline to scroll position is the most powerful narrative tool on the web, and the easiest one to ruin. The discipline that keeps it readable.

Scroll storytelling: pinning, scrubbing, and the discipline of not overdoing it — cover
A pinned scene only earns its place if it knows exactly when to let go.

Scroll storytelling gets talked about as one technique when it's really three, conflated so often that most bad implementations trace back to using the wrong one. A reveal fires once and lets go. A scrub ties an animation's progress directly to scroll position, reversible, always in sync with the user's hand. A pin holds an element in place while the page keeps scrolling underneath it, playing out a scene against scroll that would otherwise have already passed by. Each is a different promise to the user, and the sites that get scroll storytelling wrong are almost always making one promise while implementing another.

Three different things called scroll animation

A reveal — an element fading or sliding in once it crosses the viewport — is the lightest promise: I noticed you arrived here. It fires, it finishes, it gets out of the way, and the user's own scroll speed never has to match anything on screen. Most of what a page needs is this and nothing more.

A scrub is a stronger claim: this animation's progress is your scroll position, exactly, forwards and backwards. Scrubbed motion should feel like it belongs to the user's hand, not to a timer — pause mid-scroll and the animation pauses with it. It's the right tool when the content genuinely benefits from that direct coupling: a diagram assembling itself, a number counting up against a chart.

A pin is the strongest claim of the three: stay here, the story isn't done. The element stops scrolling with the page and a timeline plays out against continued scroll input instead. It's the most powerful tool available and the one most often reached for when a reveal would have done the job better, because a pin that doesn't need to exist reads to the user as the page getting stuck.

The physics problem underneath all of it

Raw scroll events are noisier than they feel. A trackpad fires small, inconsistent deltas; a mouse wheel chunks in fixed steps; momentum and inertia behave differently across operating systems and browsers. Scrub an animation directly off that raw signal and it stutters exactly when it's least forgivable — mid-motion, where the eye is already tracking it.

This is why a smoothing layer like Lenis is almost always paired with ScrollTrigger rather than optional. Lenis turns raw, jittery scroll input into a single smoothed virtual scroll value; ScrollTrigger scrubs against that value instead of the DOM's native scrollTop. The animation ends up tracking a continuous number, not a series of jumps, and the difference is the entire gap between motion that feels physical and motion that feels broken.

The other half of the physics problem is which properties you animate. Transform and opacity run on the compositor and can hold 60fps through almost anything. Animate width, top, or anything that forces layout on every scroll tick, and the frame rate collapses precisely during the scene meant to be most impressive.

The rule of arrival

Every pinned scene needs enough scroll distance to breathe, and the failure shows up at both ends of that budget. Too little distance and the scene resolves before the user has parsed what's happening — a pin that unlocks itself feels like an accident, not a payoff. Too much distance and the user is scrolling and scrolling with nothing changing, which reads as the page being stuck, the single worst thing a pinned section can communicate.

A pinned section is a section the user cannot leave until it's finished. That is either the most powerful tool on the page, or the most hostile one.

The only reliable way to tune that budget is to test at the scroll speed of an impatient user, not the careful, deliberate scroll of someone building the thing. Fling the trackpad the way a visitor actually will on their third scroll of the day, and any pin that overstays its welcome becomes obvious immediately.

Accessibility and the mobile problem

prefers-reduced-motion isn't optional here — it's the difference between a scene and a hazard for a user who's told the browser that scroll-linked motion causes them real discomfort. Every pinned or scrubbed sequence we ship has a fallback path that collapses to a simple, static reveal, checked before the ScrollTrigger instance is even created, not disabled after the fact with a CSS override.

Mobile brings a separate, more mundane failure: iOS Safari resizes its viewport as the address bar shows and hides mid-scroll, which can desynchronise a pin's calculated height from what's actually on screen and leave a scene stuck half-finished. ScrollTrigger's refresh handling covers most of this, but the safer default on small viewports is often to skip pinning entirely — a linear, unpinned sequence of reveals usually communicates the same content just as well on a phone, without fighting a viewport that won't sit still.

The budget we actually use

In practice: reveals are the default for everything, scrubs are reserved for content that genuinely benefits from tracking the user's hand, and pins are capped hard — most of the sites we build use one, occasionally two, reserved for the single moment in the page that has earned a locked stage. Anything past that and the pins start competing with each other for a scarcity of user patience that doesn't renew.

The tools for building any of this are the same ones covered in our comparison of GSAP, Framer Motion and CSS — ScrollTrigger and Lenis do the choreography here specifically because scroll storytelling is a sequencing problem, not a component-state problem. The craft isn't in knowing how to pin a section. It's in having the restraint to leave most sections unpinned.

Frequently asked questions

What's the difference between scroll scrubbing and a scroll-triggered reveal?

A reveal fires once when an element enters the viewport and then finishes, independent of further scrolling. A scrub ties an animation's progress directly and continuously to scroll position, so it plays forwards and backwards exactly as the user scrolls. Reveals suit most content; scrubbing suits animations that should feel physically connected to the user's own scroll input.

Why pair GSAP ScrollTrigger with a smooth-scroll library like Lenis?

Raw browser scroll events are noisy — trackpads and mouse wheels fire inconsistent, chunky deltas that make scrubbed animation stutter. Lenis smooths raw input into a single continuous virtual scroll value, and ScrollTrigger scrubs against that instead, which is what makes scroll-linked motion feel physical rather than jerky.

Is scroll-pinned animation accessible?

It can be, with two safeguards: a prefers-reduced-motion fallback that replaces the pinned sequence with a simple static reveal, checked before the animation is built rather than patched on after; and generous, tested scroll distance so the section never traps an impatient user without visible progress. Neither is optional in production work.

Keep reading

GSAP vs Framer Motion vs CSS animations — cover

Motion9 min read

GSAP vs Framer Motion vs CSS animations

Motion that earns its milliseconds — cover

Motion5 min read

Motion that earns its milliseconds