Scroll-Driven Animations

Published on

For years, scroll-linked effects meant shipping a JavaScript library — Intersection Observer hacks, requestAnimationFrame loops, or 45KB plugins like ScrollMagic. CSS scroll-driven animations replace all of that with two functions: scroll() and view().

The Core Idea

Instead of time driving your animation, scroll position drives it. You bind a standard @keyframes animation to a scroll timeline, and the browser handles everything on the compositor thread — 60fps with zero JavaScript.

Two timeline types exist:

  • scroll() — tracks how far a container has scrolled
  • view() — tracks an element's visibility in the viewport

Reading Progress Bar

A fixed bar that fills as the user scrolls down the page:

.progress-bar { position: fixed; top: 0; left: 0; height: 4px; width: 100%; background: var(--color-accent); transform-origin: left; animation: grow-progress auto linear; animation-timeline: scroll(root); } @keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
<div class="progress-bar" aria-hidden="true"></div>

Using scaleX instead of width keeps the animation on the compositor thread — no layout thrashing on every scroll tick.

Fade-In on Scroll

Each card fades in and slides up as it enters the viewport:

.card { animation: fade-in-up auto ease-out both; animation-timeline: view(); animation-range: entry 10% entry 90%; } @keyframes fade-in-up { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } }

The animation-range: entry 10% entry 90% controls when the animation starts and ends relative to the element entering the scrollport. No more fiddling with Intersection Observer thresholds.

Parallax

.parallax-bg { animation: parallax auto linear both; animation-timeline: view(); animation-range: cover 0% cover 100%; } @keyframes parallax { from { transform: translateY(-50px); } to { transform: translateY(50px); } }

Gotchas

  1. animation-duration: auto is mandatory. The default 0s makes scroll-driven animations invisible. Always set auto (or use the shorthand: animation: name auto timing).

  2. animation-timeline is not part of the animation shorthand. Always declare it separately.

  3. Use animation-fill-mode: both for view timelines — without it, elements snap to their un-animated state before entry and after exit.

  4. Respect reduced motion:

@media (prefers-reduced-motion: reduce) { .card, .progress-bar, .parallax-bg { animation: none; } }

Browser Support

Use @supports for progressive enhancement — the page works without the animations, they just add polish:

@supports (animation-timeline: scroll()) { /* scroll-driven styles here */ }

Chrome 115+, Edge 115+, Firefox 128+. Safari does not support this yet (as of early 2026).