Every scroll effect I've built — reading progress bars, sticky nav transitions, fade-in-on-scroll
cards — has needed JavaScript. Either a scroll event listener on window (which runs
on the main thread and can cause jank), or an IntersectionObserver (better, but
still wiring up callbacks, managing thresholds, and imperatively setting classes).
The Scroll-Driven Animations spec, now available in Chromium/Edge browsers and shipping to others, changes the model entirely. You define the animation in CSS. You tie it to a scroll timeline in CSS. The browser handles the rest — off the main thread.
The two timeline types
There are two kinds of scroll timelines:
scroll()— ties animation progress to the scroll position of a scrolling container. As you scroll down the page, the animation plays forward.view()— ties animation progress to how much of an element is visible in the viewport. The animation plays as the element enters and/or exits view.
Example 1: Reading progress bar
This is the classic use case. A thin bar at the top of the page that fills as you scroll. With JavaScript, you'd listen to scroll events and update a width or transform. With Scroll-Driven Animations:
@keyframes progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: oklch(0.7 0.2 220);
transform-origin: left;
animation: progress linear;
animation-timeline: scroll();
}
That's it. No JavaScript. The animation-timeline: scroll() declaration
tells the browser to use the scroll position of the nearest scrolling ancestor as the
animation timeline. The browser maps scroll progress (0% to 100%) directly to animation
progress (from → to).
Example 2: Element fade-in on scroll
The other common pattern: a card or section that fades in as it enters the viewport.
This is where view() comes in.
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(1.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fade-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 30%;
}
animation-range: entry 0% entry 30% tells the browser to play the animation
during the first 30% of the element's entry into the viewport. Once 30% of the way in,
the animation is at its "to" state and stays there. The element reveals itself as it
scrolls into view — exactly what you'd have used IntersectionObserver for.
both keyword in the
animation shorthand. It keeps the element at the "from" state before it enters the viewport,
and at the "to" state after the animation completes.
The performance story
This is the part I find most interesting. Scroll-Driven Animations run on the compositor thread, not the main thread. Traditional scroll event listeners run on the main thread — which means if your JavaScript is busy doing something else, scroll-driven effects can stutter.
IntersectionObserver helps, but it still fires callbacks on the main thread and
you still have to imperatively update classes or styles. The browser can't predict what
you're going to do in the callback, so it can't optimize it.
With Scroll-Driven Animations, the browser knows the entire motion upfront — it's all declared in CSS. The compositor can handle it independently of whatever JavaScript is doing on the main thread. The result is scroll effects that don't jank, even on pages with expensive JavaScript.
Browser support
As of early 2026, Scroll-Driven Animations are fully supported in Chrome 115+, Edge 115+, and Opera. Firefox is behind a flag (dom.animations-api.scroll-driven.enabled). Safari support is in development.
The graceful degradation here is that elements simply don't animate — they appear at their
final state immediately. That's acceptable for most uses. For anything where the animation
is essential to understanding the UI, add a @supports check.
I keep expecting a catch with this API. There isn't really one — it does what it says, it performs better than the alternatives, and the CSS is actually readable. Features like this one make me feel like the platform is genuinely moving in a good direction. Building things that felt like they needed a library a few years ago now just... don't.