CSSScroll-Driven Animations

Scroll-Driven Animations

Scroll-driven animations link an animation's progress directly to scroll position — instead of time, the timeline is the scrollbar. Scroll a bit, the animation advances a bit; scroll back, it rewinds. For years this required a JavaScript scroll listener recalculating styles on every frame (a common performance bottleneck). Modern CSS does it natively with the animation-timeline property, driven entirely by the compositor thread, with no JavaScript and no per-frame scroll handler at all.

Two kinds of timeline

Scroll progress timeline

View progress timeline

Driven by

How far a scroll container has scrolled

How far an element has traveled through the viewport

Function

scroll()

view()

0% to 100%

Container scroll starts to scroll ends

Element enters the viewport to element exits the viewport

Typical use

A reading progress bar tied to the whole page

An element fading or sliding in as it scrolls into view

Scroll progress: a reading progress bar

CSS
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background: #0066cc;
  width: 100%;
  transform-origin: left;

  /* Link this element's animation to the page's scroll position */
  animation-name: grow-progress;
  animation-timeline: scroll(root);
  /* "root" = the document itself is the scroll container */
}

@keyframes grow-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

There is no animation-duration here — the scroll position itself IS the timeline. At the top of the page the keyframe is at 0% (scaleX(0)); scrolled all the way to the bottom, it's at 100% (scaleX(1)). Scrolling up rewinds the bar instantly.

View progress: fade in as it enters the viewport

CSS
.card {
  animation-name: fade-in-up;
  animation-timeline: view();
  /* Timeline covers this element's journey through the viewport */

  animation-range: entry 0% cover 40%;
  /* Animation plays only while the element is entering,
     finishing once it's 40% into the viewport */
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

As each .card scrolls up into view, it fades in and slides up — purely from CSS, tied to that specific element's visibility, not a global scroll position. Scroll back up and it reverses just as smoothly.

Why this matters
  • Runs off the main thread — no jank from JavaScript scroll listeners fighting layout and paint.

  • Declarative — the relationship between scroll and animation state lives entirely in CSS.

  • Naturally reversible — scrolling back up just rewinds the timeline, no extra logic needed.

Check current browser support
Scroll-driven animations (animation-timeline, scroll(), view()) are a newer CSS capability. Support has been landing steadily in Chromium-based browsers, with other engines catching up — always check caniuse.com for the current picture before relying on this for anything critical, and provide a static fallback (the element just renders in its final state) for browsers that don't recognize animation-timeline.
Related
For animating between two DOM states without hand-written keyframes at all, see the [View Transitions API](/css/view-transitions).