Scroll Driven Animation, and Whether to Use It
Scroll timelines moved the work off the main thread and made scroll motion cheap. A working example with its reduced motion fallback, and a rule for when it earns its place.
Date
Category
Writer

For about fifteen years the standard way to make something move as you scrolled was to attach a listener to the scroll event, read a scroll offset, do some arithmetic, and write a style. The listener ran on the main thread. Scrolling itself, on every modern browser, does not. So the page scrolled at one rate and your animation caught up at another, and on a loaded page the gap was visible. Chrome's own documentation is direct about the cause: "Main thread animations are subject to jank."
The CSS solution shipped in Chrome 115, in July 2023. It is worth understanding precisely, because a good deal of what is said about it in tutorials is either out of date on support or vague on what the specification actually defines.
What the specification defines
The relevant document is the CSS Scroll-driven Animations Module Level 1, still an editor's draft at the W3C. It does not add a new animation system. It adds new timelines, and then lets the existing CSS Animations and Web Animations machinery run against them.
That framing matters. A CSS animation normally advances against a document timeline measured in milliseconds. This specification supplies two alternative sources of progress, and everything else about keyframes, easing, fill modes and composition works as it already did.
Scroll progress timelines
The spec defines these as "timelines linked to progress in the scroll position of a scroll container along a scrollable axis". The startmost scroll position is 0 per cent progress, the endmost is 100 per cent. That is the whole idea. An animation bound to a scroll progress timeline is at its first keyframe when the scroller is at the top and its last when it is at the bottom.
Declaratively you either name a timeline on the scroll container with scroll-timeline-name and scroll-timeline-axis (or the scroll-timeline shorthand), or you use the anonymous scroll() function directly in animation-timeline, which the spec describes as specifying "a scroll progress timeline". The function takes an optional scroller and axis, so scroll(root block) means the document's own block-direction scroll.
View progress timelines
These are the more useful of the two and the harder to describe. The specification defines a view progress timeline as a segment of a scroll progress timeline "scoped to the scroll positions in which any part of the subject element's principal box intersects its nearest ancestor scrollport".
In plainer terms: progress runs from the moment the element first touches the edge of the scrolling viewport to the moment it has completely left at the other edge. This is what you want for anything that should react to its own arrival, and it is declared with view-timeline-name, view-timeline-axis and view-timeline-inset, or the anonymous view() function.
view-timeline-inset is the detail people miss. The spec describes it as "an inward offset from the corresponding edge of the scrollport", which lets you say that an element should not count as entering until it is, say, 200 pixels inside the viewport. Without it, view timelines start the instant a single pixel crosses the edge, which is almost never when you want the motion to begin.
Two more pieces complete the model. animation-range restricts which part of the timeline the animation occupies, and timeline-scope lets a named timeline be visible to elements outside the subtree that declared it, which is the escape hatch for the fact that timeline names otherwise only reach descendants.
Why it moved off the main thread
The performance argument is not that CSS is faster than JavaScript. It is that a declarative timeline can be handed to the compositor, which already owns the scroll position, so the animation is evaluated in the same place and at the same moment as the scroll it depends on. There is no round trip through script.
Chrome's documentation states the outcome as: "silky smooth animations, driven by scroll, running off the main thread, with just a few lines of extra code." This inherits from how CSS Animations and the Web Animations API already work, which is why the specification is written as an extension to those rather than as a new API surface.
One honest qualification. Off the main thread is a property of the animation, not of the page. If your keyframes animate properties the compositor cannot handle alone, you have moved the timeline off the main thread and left the work on it. The usual rule applies: transforms and opacity compose cheaply, geometry does not.
Where it shipped, and where it did not
This is the part most often reported wrong, and I got it wrong myself for a while, having assumed that something in a Chrome stable release for two years would have reached everywhere by now.
Engine | Status | Detail |
|---|---|---|
Chrome | Shipped | Version 115, July 2023 |
Edge | Shipped | Version 115, mirroring Chrome |
Safari | Shipped | Version 26.0, released 15 September 2025 |
Safari on iOS | Shipped | Version 26.0, mirroring Safari |
Firefox | Not in release | Enabled by default in Nightly from version 136; off elsewhere behind a preference |
MDN's browser compatibility data lists Firefox support for animation-timeline as preview only. MDN's experimental features page gives the mechanism: scroll-driven animations are enabled by default in Nightly from Firefox 136, and available but disabled on Developer Edition, Beta and Release since Firefox 110, controlled by the preference layout.css.scroll-driven-animations.enabled.
So as of early 2026 this is a two-engine feature with a third engine holding it behind a flag. Browser support tables that show a green Firefox column are showing a projected version number, not a shipped one. Check the preference name, not the table.
The good news is that the degradation is clean. An unsupported animation-timeline value means the animation is simply not bound to that timeline. If you write your keyframes so that the static state is the end state, a Firefox release user sees the finished layout with no motion, which is very close to what a reduced-motion user should see anyway.
Feature detection, and the JavaScript side
Because the CSS degrades by ignoring an unknown animation-timeline value, most of the time you do not need to detect anything. When you do, the ordinary feature query works, and it is worth writing because it makes the two states explicit to whoever reads the file next.
There is also a scripted equivalent. The same work shipped in Chrome 115 as extensions to the Web Animations specification, which web.dev's platform round-up for July 2023 recorded as "the ScrollTimeline and ViewTimeline extensions to the Web Animations specification". Those are constructors you can pass as the timeline option to Element.animate(), which is the route to take when the ranges have to be computed rather than written.
I would reach for the JavaScript form rarely. The whole benefit of the declarative version is that the browser owns the relationship between scroll position and animation progress. Once you are computing offsets in script you have partially reintroduced the coordination problem the feature exists to remove, even though the timeline object itself still lives on the compositor side.
A working example
Here is a view timeline with the fallback written correctly, meaning that the animation is inside the reduced-motion query rather than being overridden by it.
Two things about that shape. First, the final state is declared outside any query and outside any animation, so it is what an unsupporting engine and a reduced-motion user both get. Second, the animation lives inside prefers-reduced-motion: no-preference rather than being cancelled inside reduce. WebKit's own guide recommends exactly this pattern, advising that "When in doubt, it's a good idea to wrap your animation in a media query that checks for reduced motion preferences", and showing the animation nested inside @media not (prefers-reduced-motion).
That ordering is not pedantry. If you write the animation unconditionally and switch it off in a reduce block, every future edit is one specificity accident away from shipping motion to someone who asked not to receive it.
The accessibility conditions
prefers-reduced-motion has been available since Chrome 74, Edge 79, Firefox 63 and Safari 10.1, which means it predates scroll-driven animations by several years and there is no excuse for omitting it. Thomas Steiner's web.dev article on it, from March 2019, describes the stakes plainly: vestibular reactions include "dizziness, nausea, and migraine headaches, and sometimes require bed rest to recover".
WCAG addresses this directly at Success Criterion 2.3.3, Animation from Interactions, which is Level AAA: "Motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed." The understanding document draws the distinction that matters here. Scrolling itself is fine, because "Moving new content into the viewport is essential for scrolling. The user controls the essential scrolling movement so it is allowed." What is not fine is the additional motion layered on top of it, and parallax is named as the archetypal problem case.
WebKit's guide adds a useful scale heuristic: the larger the area the animation covers, the worse it is likely to be, because "Users with motion sensitivity are more likely to experience these larger animations as real movement in three-dimensional space."
The user is already performing a motion. Anything you add is motion they did not ask for, layered onto motion they did.
A rule for when it earns its place
I have settled on one test, and it is deliberately restrictive.
There is a second reason to be conservative that has nothing to do with accessibility. Scroll-driven motion makes the page's appearance a function of a value the reader controls continuously and imprecisely. A time-based animation has one correct end state and gets there. A scroll-driven one has as many states as there are scroll positions, and every one of them has to be a state you are willing to ship. That is a much larger surface to design, and in practice almost nobody designs it. They design the start and the end, and let the middle be whatever interpolation produces.
That last failure condition is the one I would add to any published guidance, because it is the test the technology makes newly relevant. A time-based animation always completes. A scroll-driven one is wherever the reader stopped. If a reader halts halfway down and the page is a set of half-faded, half-slid elements, you have not made an animation. You have made a layout that only looks correct while someone is moving.
There is a real gain underneath all of this. Effects that previously required a scroll listener, a resize observer, a requestAnimationFrame loop and a body of code keeping them in sync are now four lines of CSS that the compositor evaluates. The reading-progress bar in particular went from a small engineering task to a declaration. That is a genuine simplification, and it arrived with a two-engine support story and an accessibility obligation attached. Both are worth stating whenever the feature is recommended.
Sources
W3C CSS Working Group, Scroll-driven Animations Module Level 1, editor's draft. Definitions of scroll progress timelines, view progress timelines, scroll-timeline, view-timeline, view-timeline-inset, timeline-scope, scroll() and view(). drafts.csswg.org/scroll-animations-1/
Chrome for Developers, "Scroll-driven animations", Chrome developer documentation. Chrome 115 availability, the main thread jank statement, and the off-main-thread claim. developer.chrome.com/docs/css-ui/scroll-driven-animations
"New to the web platform in July", web.dev blog, 29 July 2023, noting ScrollTimeline and ViewTimeline as Web Animations extensions in Chrome 115. web.dev/blog/web-platform-07-2023
MDN Web Docs, "animation-timeline", including browser compatibility data. developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline
MDN Web Docs, "Experimental features in Firefox", for scroll-driven animations enabled by default in Nightly from Firefox 136 and the layout.css.scroll-driven-animations.enabled preference. developer.mozilla.org/en-US/docs/Mozilla/Firefox/Experimental_features
WebKit, "WebKit Features in Safari 26.0", 15 September 2025, for scroll-driven animations shipping in Safari 26.0. webkit.org/blog/17333/webkit-features-in-safari-26-0/
WebKit, "A guide to Scroll-driven Animations with just CSS", for the reduced-motion pattern and the observation about large animations and motion sensitivity. webkit.org/blog/17101/a-guide-to-scroll-driven-animations-with-just-css/
Thomas Steiner, "prefers-reduced-motion: Sometimes less movement is more", web.dev, 11 March 2019, for browser support and the description of vestibular reactions. web.dev/articles/prefers-reduced-motion
W3C Web Accessibility Initiative, "Understanding Success Criterion 2.3.3: Animation from Interactions", WCAG 2.2, Level AAA. w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html
Related reading
Latest Articles.
© Moc Mallen Studio
Thoughts, ideas, and perspectives on design, simplicity, and creative process.

