This note has an interactive demo. Try it in the original
Reading progress bars, cards that slide in as they appear, a hero that fades while you scroll past it: all of that can now be plain CSS in every major browser. Firefox 159 shipped on October 27, 2026 with scroll-driven animations enabled by default, and that same day the feature became Baseline. In the demo above, scroll inside the panel: the bar, the percentage, the 3D ring and the cards all follow your scroll, and there isn't a single scroll listener in the page.
What changes
A normal CSS animation runs on the document timeline: it moves from the first keyframe to the last as time passes. Scroll-driven animations swap that clock for a different one with the animation-timeline property. There are two kinds of timeline:
- Scroll progress timeline, with
scroll(): progress goes from 0% to 100% as a scroll container scrolls from top to bottom. - View progress timeline, with
view(): progress depends on how far an element (the subject) has travelled through the visible area of its scroll container.
You keep writing the same @keyframes you already know. What changes is what drives them. And because it's declarative, you don't need scroll events, requestAnimationFrame or an IntersectionObserver to sync anything.
How it works
The progress bar at the top of the demo is the classic example:
.progress { transform-origin: 0 50%; transform: scaleX(0); animation: grow 1ms linear both; animation-timeline: scroll(nearest block); /* after the shorthand */ } @keyframes grow { to { transform: scaleX(1); } } scroll() takes two optional arguments: which scroller to use (nearest, the default; root for the document; or self) and which axis (block by default, or inline, x, y). Two details in that block matter:
- The order. The
animationshorthand resetsanimation-timelinetoauto, but it can't set it. MDN says it plainly: declareanimation-timelineafter anyanimationshorthand, or it won't take effect. - The
1ms. On a scroll timeline the duration doesn't control anything, but according to MDN, Firefox needs a non-zeroanimation-durationto apply the animation. That's why1msis the common convention.
The cards use view(), and animation-range decides which stretch of their journey the animation happens in:
.card { animation: reveal 1ms linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; } @keyframes reveal { from { opacity: 0; transform: translateY(60px) rotateX(-55deg) scale(.86); } to { opacity: 1; transform: none; } } The spec defines named ranges for a view timeline. cover is the whole journey, from the moment the element starts to enter until it has completely left. contain is the stretch where it's fully inside the visible area. entry and exit are the stretches where it's entering and leaving. The buttons at the top of the demo switch between entry, cover 0% cover 50% and contain. With the same scroll position, the same card ends up at a different opacity, because each range maps that position to a different point in the animation.
You can also give a timeline a name and use it from another element. The panel declares scroll-timeline: --page block, and the 3D ring and the counter use animation-timeline: --page. The counter is a little trick: an integer registered with @property, animated from 0 to 100 and printed with counter(). The percentage you see is computed by the browser while you scroll.
From JavaScript you get the same timeline. This is the line that feeds the readout in the demo:
const timeline = new ScrollTimeline({ source: viewport, axis: 'block' }); timeline.currentTime; // CSSUnitValue in percent, e.g. 42% The spec defines currentTime as a percentage CSSUnitValue, and as null when the timeline is inactive.
Browser support and gotchas
- Support: Chrome and Edge 115, Safari 26 (the release notes say "Added support for Scroll-driven Animations") and Firefox 159. According to the web-features data, it has been Baseline "newly available" since October 27, 2026. Anyone who hasn't updated still won't see it, so detect it: the demo checks
CSS.supports('animation-timeline: scroll()')and, if that fails, shows a notice and uses a JavaScript fallback with a scroll listener and anIntersectionObserver. In CSS, the equivalent is@supports (animation-timeline: scroll()). -
overflow: hiddencreates a scroller. This one caught me while building the demo. The bar's track hadoverflow: hiddento round its corners, and in Chromium the bar stayed at zero. MDN's definition explains why:hiddenturns an element into a scroll container, soscroll(nearest)was finding the track (which doesn't scroll) instead of the panel. Switching tooverflow: clipfixed it, becauseclipdoesn't create a scroll container. - The shorthand doesn't set the timeline. In MDN's compatibility data,
animation-timelineas part ofanimationis only "reset-only" in Chromium and not supported in Firefox or Safari. Always write it as its own declaration. - Motion still needs a switch. MDN reminds you to respect
prefers-reduced-motion. In the demo, with reduced motion on, the ring and the hero text stop moving and the cards only fade in, with no 3D rotation. The progress bar stays, because it doesn't move anything across the screen.
What to keep an eye on
- "Widely available" status. Baseline "newly available" means every engine ships it, not that every user has updated. Keep the fallback for a while.
- Your scroll libraries. If you load a library only for progress bars or reveal-on-scroll effects, it's worth checking how much of it CSS now covers.
- The spec is still an Editor's Draft. The CSSWG document is marked as "revising" and still has open issues, for example about what 0% means for some scroll containers. The basics shown here work today in all three engines, but there are still details to settle.
Sources
- animation-timeline – MDN (source on GitHub)
- Scroll-driven animation timelines – MDN guide (source on GitHub)
- Firefox 159 release notes for developers – MDN (source on GitHub)
- Safari 26 release notes – Apple Developer
- Scroll-driven Animations, Editor's Draft source – CSS Working Group
Originally published at richi.codes.

