easy animationgsapscrollscrollytelling 5 min read

Scroll-driven motion

GSAP + ScrollTrigger in 60 seconds. Build motion that reacts to the reader, not the clock.

Docs ↗

Why scroll-driven animation wins

Most web animation is time-driven: click a button, wait 800ms, see the result. That works, but it puts the interface in charge. Scroll-driven animation flips the relationship — the reader sets the pace, and the page responds.

The canonical use case is a data-viz case study. You have a chart that changes over time, a narrative that explains it, and a reader who needs to sit with each stage before moving on. Time-driven would flash each stage for a fixed duration and lose the slow reader and bore the fast one. Scroll-driven lets each reader spend exactly as long as they need — the animation is a scrubber, they’re the play head.

GSAP’s ScrollTrigger plugin is the de-facto tool here. It’s ~15 KB gzipped, Web-Animations-API-adjacent, and the API is so thin you can’t get lost.

Three concepts in three snippets

1. from() — entrance animation on load

gsap.from('#cards .card', {
  y: 60,
  opacity: 0,
  duration: 0.8,
  stagger: 0.1,
  ease: 'power3.out',
});

from() means “animate FROM these values TO the current values in the stylesheet.” The cards exist in their final position on render; GSAP pulls them 60px down, fades them out, then animates back over 800ms with a 100ms stagger between each. You don’t touch CSS — the tween owns the transition.

2. scrollTrigger.scrub — sync motion to scroll position

gsap.to('#cards .card:nth-child(2)', {
  rotation: 360,
  scrollTrigger: {
    trigger: '#scrollzone',
    start: 'top center',
    end: 'bottom center',
    scrub: true,
  },
});

scrub: true is the magic word. Without it, the animation plays once when #scrollzone enters the viewport. With it, the animation’s progress is tied to the user’s scroll progress through that range. Scroll up → rotation reverses. Scroll fast → rotation is fast. This is the scrollytelling primitive.

3. pin — hold an element while content flows past

gsap.timeline({
  scrollTrigger: {
    trigger: '#scrollzone h2',
    pin: '#scrollzone h2',
    start: 'top 20%',
    end: '+=200',
    scrub: 1,
  },
}).to('#scrollzone h2', { scale: 1.2, color: '#F5A623' });

Pinning is how you keep a header visible while the reader scrolls through its explanation. The headline sticks at 20% from the top for 200px of scroll, animating up to 1.2× size and amber as it does. Once the scroll completes, the pin releases and the headline flows away normally.

scrub: 1 (not true) adds 1 second of smoothing — the animation catches up over 1s instead of snapping. It’s the difference between a scroll that feels responsive and one that feels designed.

When NOT to use ScrollTrigger

Ship notes

GSAP is free for commercial use as of v3.13+ (July 2024 license change — previously required a Club GreenSock membership for some plugins). ScrollTrigger is in the free tier. For scroll-driven pinning and scrubbing at the scale we use, this is the best tool by a margin.

VORLUX’s front page hero uses exactly this primitive — the inference-viz animation advances as you scroll the hero into view. Worth clicking around on any page with a scrollytelling section (our case studies use it heavily) and watching the network tab: you’ll see GSAP + ScrollTrigger pulled from the gsap package on our bundle, roughly 22 KB gzipped for the pair.

Go remix the playground — change scrub: true to scrub: 1, swap power3.out for elastic.out(1, 0.4), pin the whole #cards container instead of just the headline. Each tweak teaches something that reading docs doesn’t.