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
- Short content with a single CTA. Use a time-driven entrance animation and stop.
- Anything that has to be accessible via keyboard or screen reader. Scroll-based animation can confuse assistive tech if the motion carries meaning. Pair with
prefers-reduced-motionand ensure the narrative works with motion off. - Mobile with heavy GPU work. Scrub animations on weak devices can stutter. Profile before shipping a long scrolly-piece to mobile.
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.