Scroll-linked effects — progress bars, parallax, reveal-on-scroll — have historically required JavaScript computing animation state on every scroll event. CSS scroll-driven animations bring this capability natively into the browser.
1Scroll Position Replaces Elapsed Time
A standard CSS animation is driven by a time-based timeline: it starts, and its keyframes progress based on elapsed seconds, regardless of anything else happening on the page. animation-timeline: scroll() swaps that time-based driver for a scroll-position-based one: the animation's percentage progress now maps directly and instantly to how far the specified scroll container has been scrolled, from 0% at the top to 100% at the bottom (or whichever axis and range is configured).
This is a genuinely different kind of timeline, not just a new easing or trigger — the animation has no concept of 'time' at all in this mode; it's purely a function of scroll position, which is why scrolling up instantly reverses the animation's progress too, with no special handling required.
2view(): Animating Based On An Element's Own Visibility
A distinct but related timeline type, animation-timeline: view(), instead derives progress from where the animated element itself currently sits relative to its scroll container's visible viewport — as the element scrolls into view, crosses through it, and eventually scrolls back out, the animation progresses through that same journey.
This is exactly the mechanism behind the extremely common 'fade and rise into view as you scroll to this section' effect, and the animation-range property lets you control precisely which portion of that entry/exit journey the animation should actually cover — for example, completing entirely within the first 40% of the element's entry into view, rather than continuing to animate throughout its entire time on screen.
3What This Replaces: A Main-Thread-Heavy JavaScript Pattern
Before scroll-driven animations existed natively, achieving these exact effects required a JavaScript scroll event listener (often combined with IntersectionObserver for visibility detection), manually computing a progress value on every single scroll tick and writing it either directly to a style property or to a CSS custom property consumed elsewhere. This runs continuously on the main thread throughout the entire scroll interaction, competing for time with everything else JavaScript needs to do.
Native scroll-driven animations let the browser's compositor handle this computation directly, without continuous main-thread JavaScript execution — connecting directly back to the GPU Acceleration lesson's theme of moving work off the CPU's critical path wherever possible, applied here specifically to scroll-linked effects.
4Step-by-Step Breakdown
Scroll Position As The Animation Clock. A normal CSS animation runs on a time-based clock — seconds ticking forward regardless of what the user does. Scroll-driven animations replace that clock entirely: the animation's progress is driven directly by scroll position, natively, with zero JavaScript and zero scroll-event listener.
Replacing Time With Scroll Position. animation-timeline: scroll() tells an animation to derive its progress from a scroll container's position instead of elapsed time — 0% scrolled maps to the animation's 0% keyframe, 100% scrolled maps to 100%, and everything in between tracks proportionally and instantly as the user scrolls.
The Timeline Swap. What does animation-timeline: scroll() change about how an animation's progress is determined?
- →It only changes how fast the animation plays, not what drives it
- →The animation's progress is now driven by scroll position instead of elapsed time
- →It has no effect without additional JavaScript
view() Timeline: Animating Based On Visibility. A related but distinct timeline, animation-timeline: view(), drives an animation based on an element's own position within its scroll container's visible viewport — as the element enters, crosses, and exits view, the animation progresses, ideal for 'fade and rise in as you scroll to it' effects on individual elements.
scroll() vs view(). What's the key difference between animation-timeline: scroll() and animation-timeline: view()?
- →They're functionally identical, just different syntax
- →scroll() tracks the scroll container's overall position; view() tracks a specific element's own visibility within the viewport
- →view() is a JavaScript-only feature, not real CSS
Why This Replaces A Common JavaScript Pattern. Before scroll-driven animations, this exact effect required a JavaScript scroll event listener (often paired with IntersectionObserver) computing progress manually on every scroll tick and writing it to a style property — a continuous, main-thread-bound cost that native scroll-driven animations eliminate by letting the compositor handle it directly.
The JavaScript It Replaces. Why is native animation-timeline: scroll() generally better for performance than the equivalent JavaScript scroll-listener pattern it replaces?
- →Purely because it results in less code to write
- →The browser can compute and apply scroll-driven animation progress without the continuous main-thread JavaScript execution a scroll listener requires
- →There's no meaningful performance difference
Scroll-Driven Animation Mastery. You now know how to drive an animation's progress directly from scroll position with scroll(), how view() drives animation based on an individual element's own visibility for scroll-reveal effects, and why this native approach eliminates a common, main-thread-heavy JavaScript scroll-listener pattern entirely.
Keep The End State After The Animation Finishes. animation-fill-mode: forwards keeps an element in its final keyframe state instead of snapping back.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Scroll-Driven Animations Must Respect prefers-reduced-motion Like Any Other Animation
A scroll-linked reveal or parallax effect is still motion, and users who've indicated a preference for reduced motion should have these effects disabled or substantially minimized, exactly as covered in the dedicated prefers-reduced-motion lesson.
2Scroll-Driven Effects Should Never Be The Sole Mechanism For Revealing Essential Content
Content that only becomes visible or legible via a scroll-timeline animation risks being missed by users who scroll quickly, use assistive technology that doesn't trigger the same visual scroll behavior, or have animations disabled — essential content should remain accessible regardless of animation state.
SEO Implications
- 1
Native Scroll-Driven Animation Reduces JavaScript Execution Time During Scroll, Improving Responsiveness Metrics
Eliminating a continuous scroll-event JavaScript handler reduces main-thread contention during scrolling, which can measurably improve interaction responsiveness metrics relevant to Core Web Vitals.
- 2
Scroll-Linked Effects Implemented Natively Avoid The Layout Thrashing Risk Common In Hand-Rolled JS Implementations
A naive JavaScript scroll-progress implementation risks interleaved reads/writes causing layout thrashing (covered in the Reflow and Repaint lesson); the native CSS feature avoids this failure mode entirely by design.
Best Practices
Prefer Native animation-timeline Over A JavaScript Scroll Listener For Any New Scroll-Linked Effect
It eliminates an entire category of main-thread performance risk and layout-thrashing bugs that hand-rolled JavaScript implementations are prone to, wherever current browser support allows it.
Use animation-range To Deliberately Control Where Within An Element's Visibility A view() Animation Plays
Without it, the animation runs across the element's entire time in the viewport, which is often not the intended, more contained reveal effect a design actually calls for.
Frequent Bugs
A scroll-driven progress bar doesn't animate at all.
Confirm the animation has a keyframe defined and animation-timeline: scroll() is applied to the correct element (often the animated element needs to be inside, or explicitly reference, the intended scrolling container).
A scroll-reveal effect using view() plays across the element's entire time in the viewport instead of completing quickly as intended.
Add an explicit animation-range to constrain the animation to a specific, narrower portion of the element's entry into view.
Real-World Examples
A Native Reading-Progress Bar
An article page's top progress bar showing how far the reader has scrolled through the content, implemented entirely with animation-timeline: scroll() and zero JavaScript.
@keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.reading-progress {
animation: grow-progress linear;
animation-timeline: scroll(root);
transform-origin: left;
}