CSS

Our first real use of CSS scroll-driven animations

CSS

CSS scroll-driven animations landed in browsers this year, letting an element's animation progress tie directly to scroll position without a JavaScript scroll listener in the loop at all. The animation timeline itself becomes a function of scroll offset rather than time, which is a genuinely different model from how CSS animations have traditionally worked, and it took a bit of adjustment to think in those terms after years of reaching for a scroll event handler by default.

A client's storytelling landing page had relied on a hand-tuned scroll listener for years, complete with the usual jank and throttling headaches that come with running JavaScript on every scroll event and trying to keep the resulting visual updates smooth across a range of devices with very different performance characteristics. Replacing it with a pure CSS animation timeline removed an entire class of performance tuning we no longer need to think about, since the browser handles the timing and rendering natively rather than us manually throttling a scroll handler and hoping the frame budget holds.

The migration wasn't purely a drop-in replacement, though. A couple of effects on the original page relied on scroll velocity, not just position, to trigger slightly different animation behavior depending on how fast someone was scrolling, and the current scroll-driven animation spec doesn't expose velocity directly. We ended up simplifying those specific effects to depend only on scroll position, which lost a small amount of the original's nuance but was a reasonable tradeoff given how much complexity it removed elsewhere on the page.

Browser support was the other real consideration. At the time of the migration, support was solid in Chromium-based browsers but still catching up elsewhere, so we kept a lightweight JavaScript fallback for browsers that don't yet support the CSS feature, detected through a straightforward feature query rather than user-agent sniffing. That fallback is simpler than the original hand-tuned listener since it only needs to cover a shrinking minority of visits, and we expect to be able to retire it entirely within the next year or so as support fills in across the remaining browsers.

Debugging the new animation timelines took some adjustment for the team too. With a scroll listener, stepping through the animation logic in dev tools was familiar territory, set a breakpoint, inspect the scroll offset, watch the resulting style changes. Scroll-driven animations move that logic into the browser's own rendering pipeline, which means there's no equivalent breakpoint to set; instead we leaned on browser dev tools' newer animation inspector panels, which show the timeline directly, and it took a bit of trial and error to build the same intuition for troubleshooting an animation that looks slightly off.

The performance difference wasn't just about removing jank, either. Because the animation now runs on the browser's compositor thread rather than the main thread, scrolling stayed smooth even while other JavaScript on the page was doing unrelated work, something the old scroll-listener implementation had never fully solved despite considerable tuning effort. That decoupling from main-thread work is arguably a bigger win for this particular page than the jank reduction alone, since the storytelling page also loads a fair number of images and other assets that used to compete with the scroll handler for the same thread.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients