September 25, 2026 · 2 min read
Scroll-driven storytelling without scroll-jacking
Pinned sections and scrubbed animation can feel incredible or feel broken — the difference is almost always whether the user is still in control.
Scroll-jacking has a bad reputation for a good reason: for years it meant intercepting the scroll wheel, overriding its speed, or trapping the user in a section until an animation finished. None of that is necessary to get the effect people actually want, which is content that responds to scroll — not content that controls it.
The rule that matters
If a user can scroll past your animation at their own pace — fast, slow, or
straight through — using nothing but the normal scroll gesture, you haven't
scroll-jacked anything. GSAP's ScrollTrigger makes this almost free to get
right, because scrub ties animation progress directly to scroll position
rather than to time: there's no "animation finishes, then scroll resumes"
state, because the animation is the scroll position.
gsap.to(".panel", {
xPercent: -100,
ease: "none",
scrollTrigger: {
trigger: ".panel",
pin: true,
scrub: true,
end: "+=1000",
},
});
pin: true holds an element in place while its scroll range is active —
that's the part people associate with scroll-jacking — but the user is still
scrolling the real page the whole time. Scroll back up, and the animation
reverses exactly as far as they scrolled. Nothing is ever waiting to catch
up or finish.
Where it actually goes wrong
Almost every bad scroll experience we've seen comes from one of three mistakes, not from pinning itself:
Scrubbing and toggleActions on the same trigger. Pick one behavior.
Mixing "animation follows scroll position" with "animation plays once on
enter" produces exactly the jumpy, fighting-the-user feeling scroll-jacking
is known for.
Pinning for longer than the content justifies. If a pinned section holds the user for three screen-heights of scroll to reveal one idea, it reads as being held hostage, not guided.
No reduced-motion escape hatch. prefers-reduced-motion: reduce should
produce the finished state immediately, with no pin and no scrub — not a
slower version of the same effect.
What we actually ship
Every scroll sequence on this site is built from the server-rendered,
finished state first — the content is fully readable with JavaScript
disabled. GSAP then rewinds it for motion-OK visitors only, scoped with
gsap.matchMedia() so reduced-motion and small-screen visitors get simple,
once-only reveals instead of a pinned sequence. The pin is an enhancement
layered on top of working content, never a requirement for the content to
exist.