Scroll-driven animation, without a line of JavaScript
Animations tied to scrolling are now plain CSS. What that changes, what it costs, and the trap we fell into while rebuilding this site.
For fifteen years, making a block appear as it entered the viewport required JavaScript. First a scroll event listener, which gave you pages that stuttered. Then IntersectionObserver, cleaner, but still code to download, execute and maintain. Almost nobody wrote it by hand: you installed a thirty to fifty kilobyte library, to fade something in.
That detour is no longer necessary. CSS can now drive an animation from an element’s position in the viewport instead of from the clock.
What the property does
An ordinary CSS animation runs on time: it lasts one second, then it is over. animation-timeline replaces that clock with something else. With view(), progress no longer depends on elapsed seconds but on where the element sits relative to the viewport: at the moment it enters, the animation is at its start; as the element rises, the animation advances; a reader scrolling back up runs it backwards.
@keyframes rise {
from { opacity: 0; transform: translateY(1.25rem); }
to { opacity: 1; transform: none; }
}
.block {
animation: rise linear both;
animation-timeline: view();
animation-range: entry 0% cover 32%;
}
That is all. No script, no observer, no class toggled from JavaScript. And, more to the point, the animation runs on the browser’s compositor thread rather than the main thread, which means it stays smooth even while the page is busy doing something else.
The line that matters most is the third one. Without animation-range, the animation is tied to the element’s entire journey across the screen: the block fades back out as it leaves through the top, which everyone reads as a bug.
Where it can be used
As of 27 July 2026 the picture is frankly lopsided.
Chrome and Edge have shipped scroll-driven animations since the summer of 2023. Safari shipped them in version 26, released in September 2025, and WebKit published a full guide. Firefox has not shipped them in a stable release: MDN states plainly that the property is not part of the common baseline, for exactly that reason.
Which does not mean it cannot be used, on one condition: the page without the animation must be a finished page.
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
/* animations live here, and nowhere else */
}
}
The resting state has to be the visible state, with the animation merely starting somewhere else and returning to it. A browser that skips the block renders the finished page. Write it the other way round, setting opacity: 0 by default and raising it afterwards, and a reader on Firefox gets a blank page. That mistake is visible every day on sites built with a badly configured library.
The second guard matters as much as the first. An entrance animation left running under prefers-reduced-motion triggers nausea in people prone to motion sickness. This is not a courtesy, it is an accessibility criterion, and it costs one line.
The trap we fell into
We rebuilt this site with the technique. The first attempt produced a half-drawn illustration, frozen, refusing to finish however we scrolled.
The explanation is obvious once found. The illustration sits in the page header, above the fold. An animation driven by view() on an element that is already visible at load time settles immediately on whatever progress its position gives it, and stays there until the page is scrolled. Since the element is near the top, that progress is roughly halfway. So the drawing stayed half traced, which looked like a rendering bug when it was in fact exactly the behaviour we had asked for.
The rule that comes out of it fits in one sentence: only drive from scroll what is genuinely further down the page. Anything visible at load time animates on the clock, with a duration and a delay, as before.
What it is worth
We replaced a scroll-reveal library with roughly eight hundred bytes of CSS. This site now loads zero JavaScript files, and has more motion than it did before.
That is the kind of advance we look for: not the one that lets you do one more thing, the one that lets you take something away.