animation-timeline · scroll() · view() — zero JavaScript, off the main thread
The progress bar and the spinning badge are both driven by the container's scroll position — zero JavaScript, no scroll listeners, running entirely on the compositor thread. Scroll the box and scrub them back and forth.
animation-timeline yet (Firefox stable keeps it behind a flag). The demos below will appear static — everything still works, which is exactly the progressive-enhancement point this tutorial teaches. Try Chrome, Edge, or Safari to see them move.animation-timeline just swaps the progress source: instead of the clock, it's the scroll position. Duration becomes meaningless (it's ignored), and scrolling backwards scrubs the animation backwards. Easing, fill modes, and delays all still work.
Each card animates based on its own visibility in the scrollport. Scroll the box: cards fade up during their entry range. The last card uses cover — it color-shifts across its entire visible journey.
I fade and slide up only while entering the scrollport. Once fully in, I'm done. Scroll me back out and in — I replay in both directions.
One CSS rule handles every card. With IntersectionObserver, this needed observer setup, threshold tuning, and class juggling per element.
animation-range: entry 0% entry 100% — the animation maps exactly onto the entry phase.
I use animation-range: cover — my border and background shift color across my ENTIRE visible journey, from first pixel in to last pixel out. Scroll slowly and watch.
animation-delay shifts where each element's animation starts within its timeline — give the 2nd, 3rd, 4th card increasing negative delays and they cascade. Alternatively, offset ranges per child: animation-range: entry 10% entry 110%.
Five sharp edges the happy-path tutorials skip — plus the production-safe pattern.
animation-timeline is a reset-only member of the animation shorthand: the shorthand always resets it to auto and can never set it. The #1 "works in isolation, breaks in my codebase" cause.
overflow: clip cuts off overflow WITHOUT establishing a scroll container — the modern replacement for the hack, and it keeps view() working against the real viewport.
The classic shipped bug: cards stuck at opacity: 0 forever in unsupporting browsers. Author the finished state as default, layer the animation inside @supports + reduced-motion guards. Worst case: graceful, never broken.
The off-main-thread superpower only applies to compositor properties. Animate layout properties on a scroll timeline and you've rebuilt scroll jank in pure CSS. Stick to transform and opacity; fake size changes with scale().
Parallax and multi-speed movement are documented vestibular-disorder triggers. Gate motion behind no-preference. Nuance: reduced motion isn't "no animation" — opacity fades and color shifts are generally fine to keep.
Scrubbing feels wrong for "play this entrance once" — that's what animation-trigger (Chrome 145+) delivers: a real timed animation, fired by visibility. The final IntersectionObserver use case, falling. Treat as enhancement-only for now.
layout.css.scroll-driven-animations.enabled flag (on in Nightly; a named Interop 2026 priority). ~83% global — close to Baseline but not quite. This is fine: scroll-driven animations are progressive enhancement by design. Unsupported browsers ignore animation-timeline and show your (correct) default state. Ship it today with the golden-rule pattern.