Scroll-Driven Motion
Animations linked to scroll position, on the compositor.
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.
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()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.
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.
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.
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.