Scroll-Driven Animations (CSS)

Tie a CSS animation's progress to scroll position instead of time, with no JavaScript and no scroll listeners. The browser runs it on the compositor thread, so it stays smooth even when the main thread is busy. This is the native replacement for IntersectionObserver reveals and many GSAP scroll effects.

The two timelines

animation-timeline swaps the default time-based timeline for a scroll-based one:

  • scroll() ties progress to a scroll container, 0% at top to 100% at bottom. Use it for document-wide effects like a reading progress bar: animation-timeline: scroll(root block). The arguments are scroller (root, nearest, self) and axis (block, inline, x, y).
  • view() tracks an element's visibility as it crosses the scrollport. Use it for reveal-on-scroll, where each card animates as it enters: animation-timeline: view(). Source: MDN CSS scroll-driven animations, https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations, 2026-05-31

Named timelines (scroll-timeline, view-timeline) let one element drive another's animation by reference (animation-timeline: --my-timeline).

Controlling the active window

animation-range defines where in the scroll the animation plays. Named ranges are cover, contain, entry, exit, and the crossing variants. Example: animation-range: entry 0% cover 40% means "play across the first 40% of the element entering the viewport," giving a crisp reveal. Source: rebeccamdeprey.com, https://rebeccamdeprey.com/blog/scroll-driven-animations-css, 2026-05-31

.card {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

both fill mode keeps the element in its final state after the range completes.

Why it beats JavaScript

It runs off the main thread at the compositor level, so it holds 60fps where requestAnimationFrame and scroll handlers jank. Bundle cost is zero versus roughly 25KB for GSAP. The trade-off is less flexibility for complex multi-step choreography. Source: dev.to grimicorn, https://dev.to/grimicorn/css-scroll-driven-animations-no-javascript-required-2f9k, 2026-05-31

Browser support and fallback (2026-05-31)

Full support in Chrome and Edge 115+, Opera 101+, and Safari 26. Firefox is partial behind the layout.css.scroll-driven-animations.enabled flag, so the feature is not yet Baseline. Source: MDN scroll() function compat, https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation-timeline/scroll, 2026-05-31

Guard with @supports and ensure content is visible by default, so unsupported browsers simply skip the motion:

@supports (animation-timeline: view()) {
  .card { animation: fade-in linear both; animation-timeline: view(); }
}

For critical reveals, fall back to IntersectionObserver where the feature is absent. Quirk: Firefox requires a nonzero animation-duration (for example 1ms) for the animation to apply.

When to still reach for GSAP

Native scroll-driven CSS owns progress bars, single-element reveals, and light parallax. Keep GSAP plus ScrollTrigger for pinned sections, scrubbed multi-step timelines, and mixed DOM and canvas storytelling. See the tool table in Motion 3D Playbook and the stack placement in Frontier Stack 2026. Centralize the @keyframes and timeline plumbing in globals.css per CSS UI Enforcement; components stay cn()-first. Always respect prefers-reduced-motion.


Timeline

  • 2026-07-02 | Deep-reviewed Jhey's animated scrollbar-color artifact as a concrete scroll-timeline micro-polish pattern: page state drives a registered hue property and the viewport scrollbar thumb updates while the transparent track disappears. Source: X/@jh3yy, 2026-05-30; Source: local contact sheet, 2026-07-02
  • 2026-06-01 | Concrete applications catalogued in Jhey CSS Techniques: scroll masking via mask-composite on scroll(self), and scrollbar-color hue synced on a view() timeline. Source: X/@jh3yy, 2026-05-30
  • 2026-05-31 | Page created. Captured scroll() vs view() timelines, named timelines, animation-range windows, the compositor-thread performance argument, the @supports plus IntersectionObserver fallback, the Firefox 1ms quirk, and the Chrome/Edge 115+ / Safari 26 / Firefox-flag support state (not yet Baseline). Source: MDN + web.dev + dev.to, 2026-05-31