Pattern 04 — animation-timeline, zero JavaScript

Scroll timelines in pure CSS

Every other page in this repo pays a JavaScript tax: listeners, observers, requestAnimationFrame bookkeeping. This page pays nothing. animation-timeline re-times ordinary CSS keyframe animations against scroll position instead of the clock — scroll() binds to a scroll container's progress, view() to an element's own journey across the viewport. The browser runs both off the main thread, which means these effects keep their frame rate even while your app is busy doing worse things.

There is no script tag in this file. View source and check.

Heads up: this browser does not support CSS scroll-driven animations yet, so the page is showing its fallback state — all content visible, no scroll effects, and the reading bar hidden rather than frozen. That degradation is itself the pattern: wrap the enhancement in @supports, keep the base state complete.

Demo 01 is already running

The blue bar pinned to the top of this page is a scroll-driven animation: a one-keyframe scaleX re-timed against the document scroller. The entire implementation:

.reading-bar span {
  transform-origin: left center;
  animation: grow-x linear both;      /* linear: meters must not ease */
  animation-timeline: scroll(root);   /* 0% = page top, 100% = bottom */
}
@keyframes grow-x {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

Compare that with the JavaScript version on the progress page — ten lines there, zero here, identical pixels. The JS version's advantage is reach: it works in every browser that runs script. Pick per audience; ship the @supports fallback either way.

Demo 02 — view(): scale on entry

This figure starts at 86% scale and low opacity, reaching full size by the time it has crossed 45% of the viewport. The timeline is the element's own visibility — no coordinates, no math, one animation-range declaration:

animation-range: entry 0% cover 45% — done while you are still looking at it.

Demo 03 — view(): slide on entry

From the left

The range here is entry 0% entry 100% — the card finishes its slide the instant its bottom edge clears the viewport's bottom. Entry ranges keep effects tied to arrival, which is where the eye expects them.

From the right

Scrub it: scroll back up. Scroll-driven animations run backwards for free — position in, position out — something the class-toggling observer pattern has to fake with re-arming logic.

Reduced motion, handled in one line

Under prefers-reduced-motion these cards swap their keyframes for a pure crossfade via animation-name: fade-only. The reading bar stays: it is an indicator that moves only as far as you scroll it, not decoration.

Browser support

CSS scroll-driven animations (animation-timeline: scroll() / view()), as of early 2026. Verify current data on caniuse.com before shipping.
BrowserStatus
Chrome / Edge 115+Supported
Opera 101+Supported
Safari 26+Supported (earlier Safari: no)
FirefoxIn development — behind the layout.css.scroll-driven-animations.enabled flag

The deployment rule that makes partial support a non-issue: author the page so it is complete without the animation, then add motion inside @supports (animation-timeline: scroll()). Never put an element's visible state behind a feature the browser might not have — the from-state belongs in the keyframes, not in the base rule. Browsers that understand the timeline get the choreography; everyone else gets a finished, static page and no way to tell anything is missing.