For years, scroll animation meant JavaScript: scroll listeners, IntersectionObserver, or a library like GSAP ScrollTrigger doing the heavy lifting. That era is ending. CSS scroll-driven animations let the browser scrub a plain CSS animation with scroll position, natively and off the main thread. When I first wrote about animation-timeline it was a Chrome-only experiment. It is now shipped in three of the four major engines, it is an Interop 2026 focus area, and you can use it in production today as a progressive enhancement. Let’s take a proper tour.
Scroll-driven vs scroll-triggered
The name matters. A scroll-driven animation is scrubbed by scroll: the user’s scroll position is the playhead, and scrolling backwards runs the animation backwards. That is what CSS gives us. A scroll-triggered animation fires once when an element enters the viewport and then plays on its own clock. CSS does not do that part yet (it is being explored for a future level of the spec), so keep IntersectionObserver around for one-shot reveals. Everything below is about the scrubbed kind.
Why this beats JavaScript
Scroll listeners run on the main thread, and janky scroll handlers are one of the classic performance foot-guns. Scroll-driven CSS animations can run on the compositor thread, which means they stay in sync with scrolling even while the main thread is busy parsing, hydrating, or running your analytics tag’s fifth retry. You write a normal @keyframes animation, swap the clock for a timeline, and the browser does the rest.
Two kinds of timeline
There are two anonymous timeline functions, and they cover most use cases.
scroll(): scroll progress timelines
scroll() maps an animation to how far a scroll container has scrolled. The classic demo is a reading progress bar, and it is three lines of meaningful CSS:
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
transform-origin: 0 50%;
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
scroll() takes two optional arguments: the scroller (nearest, root, or self) and the axis (block, inline, y, x). scroll(root block) means “the document’s vertical scroll,” which is what you want for a page progress bar.
view(): view progress timelines
view() maps an animation to an element’s journey across the scrollport: 0% as it enters one edge, 100% as it leaves the other. This is the one that replaces most IntersectionObserver reveal code:
@keyframes fade-up {
from { opacity: 0; translate: 0 2rem; }
to { opacity: 1; translate: 0 0; }
}
.card {
animation: fade-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
animation-range: the part the early articles missed
By default a view timeline runs across the element’s entire trip through the scrollport, which usually feels wrong: your fade-in is still half-finished when the element is mid-screen. animation-range restricts the animation to a slice of the timeline, and it is where scroll-driven animations start to feel designed instead of demoed. The named ranges:
cover: from first pixel entering to last pixel leaving (the default)contain: only while the element is fully inside the scrollportentry: while the element is enteringexit: while the element is leaving
You can mix names with percentages, so animation-range: entry 0% cover 40%; starts the moment the element appears and finishes at 40% of its journey. That one property is the difference between an animation that reacts and an animation that feels choreographed.
Named timelines and timeline-scope
The anonymous functions track the element’s own nearest scroller. When one element should drive an animation on a different element, give the timeline a name:
.gallery {
scroll-timeline-name: --gallery;
scroll-timeline-axis: inline;
}
.gallery-progress {
animation: grow-progress linear;
animation-timeline: --gallery;
}
The catch: the animated element must be a descendant of the element declaring the timeline for name lookup to work. When it is not, hoist the name with timeline-scope on a shared ancestor and the timeline becomes visible to the whole subtree:
.page-wrapper {
timeline-scope: --gallery;
}
Gotchas worth knowing
- Order matters. The
animationshorthand resetsanimation-timeline, so declareanimation-timelineafter the shorthand or it will silently do nothing. This still bites everyone once. - Duration is ignored, mostly. The timeline drives progress, not the clock, so
animation-durationhas no visual effect. Some older WebKit builds wanted an explicit duration, so you will still seeanimation-duration: 1ms;in the wild as a harmless belt-and-braces line. - Respect reduced motion. Scrubbed motion can be worse than timed motion for vestibular disorders. Wrap the whole thing in
@media (prefers-reduced-motion: no-preference).
Progressive enhancement with @supports
Feature detection is the same as ever:
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.card {
animation: fade-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
}
Browsers without support show static, fully visible content. No cliff, no polyfill required. There is an official polyfill if you need identical behavior everywhere, but for decorative motion I would rather ship less JavaScript.
Browser support in mid-2026
- Chrome and Edge: shipped since version 115, July 2023.
- Safari: shipped in Safari 26, with threaded (off-main-thread) scroll animations and a batch of accuracy fixes landing through the 26.x point releases.
- Firefox: still behind the
layout.css.scroll-driven-animations.enabledflag in stable, on by default in Nightly, and named as an Interop 2026 priority. The flip feels close.
Global support sits a little over 80%, which is exactly the zone where progressive enhancement shines: the majority get the polish, everyone gets the content, and the Firefox flag flipping is a free upgrade you have already shipped.
Resources
- MDN: CSS scroll-driven animations
- scroll-driven-animations.style (Bramus’s demo and tooling site, still the best gallery of patterns)
- CSSWG spec: Scroll-driven Animations Module Level 1
- caniuse: animation-timeline support tables

Leave a Reply