September 25, 2026 · 2 min read

GSAPMotionAccessibility

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.