Chapter 7

Scroll-Driven Motion

Animations linked to scroll position, on the compositor.

animation-timelineIntersectionObserverGSAP ScrollTrigger
scroll-linked vs scroll-triggered. CSS animation-timeline: scroll()/view() runs jank-free on the compositor — but is Chromium-mostly and needs fallbacks.

Most motion is timed by the clock. Scroll-driven motion throws away the clock and uses scroll position itself as the timeline — the user scrubs the animation with their finger. Done natively, it runs on the compositor thread and stays glassy even while the main thread is busy.

Two patterns: linked vs triggered

These names get blurred constantly, but they are different mechanisms with different cost profiles.

scroll-linked

Progress is continuously tied to scroll position. Scroll halfway → the animation is at 50%. Scroll back up → it rewinds. This is a parallax layer, a reading-progress bar, a scrubbed hero.

animation-timeline: scroll()
scroll-triggered

Fires once when an element crosses a threshold, then plays on its own clock and doesn't rewind. This is a fade-in-on-enter, a count-up, a one-shot reveal.

IntersectionObserver → play()

Support: this is not Baseline

The honest caveat. Native CSS scroll-driven animations shipped in Chromium and are gorgeous, but coverage is uneven — so detect and degrade.

CSS scroll-driven animations not Baseline
Chrome/Edge
Firefox
Safari

Chromium-shipped; Firefox behind layout.css.scroll-driven-animations.enabled; no Safari. ~85% reach with the scroll-timeline polyfill — but the polyfill does not cover advanced linked timelines. The newer animation-trigger property is Chrome/Edge-only.

live in your browser: detecting… → —

Demo: progress bar + parallax + reveals

Scroll inside the box (not the page). A progress bar fills, three layers drift at different rates, and the cards reveal as they enter. Toggle the engine: the CSS path uses animation-timeline: scroll()/view() on the compositor; the JS path computes progress by hand with a scroll listener and fires reveals with IntersectionObserver. If your browser lacks CSS support the toggle's CSS option is disabled and JS is used automatically.

Contained scroll scene live demo
parallax · 3 layers · JS path
enters view() entry 0% → cover
tracks opacity + translateY tied to range
stays persists once exit passes
last bottom of the contained timeline
end of timeline
scrollTop / max 0%

A scroll listener + rAF computes scrollTop / max; IntersectionObserver fires the one-shot reveals.

Playground: scrub an animation with scroll

Edit animation-range on the box — try entry, exit 50%, cover, or explicit 20% 80% — and watch where in the scroll the animation starts and ends. Swap the scroll() source or the keyframes too.