Back
6 min read

Scroll-Driven Progress Indicator

  • CSS
  • Scroll-Driven Animations
  • Progress

CSS Scroll-Driven Animations

Scroll-Driven Progress Indicator
Zero JavaScript.

CSS only animation-timeline: scroll()
Scroll this page. The progress bar above is driven entirely by CSS: no scroll event listeners, no JavaScript. The code is twelve lines.

The Technique

The scroll-driven animations API lets you tie an animation's progress directly to scroll position, replacing the most common reason to reach for JavaScript on a reading progress bar.

@supports (animation-timeline: scroll()) {
  .progress {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 3px;
    transform-origin: 0 50%;
    transform: scaleX(0);
    animation: progress linear both;
    animation-timeline: scroll(root);
  }

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

How It Works

animation-timeline: scroll(root) links the animation's progress to the root element's scroll position. As you scroll from top to bottom, the animation plays from from to to. The bar starts at scaleX(0) and ends at scaleX(1), producing the fill effect.

transform-origin: 0 50% is the key detail: it anchors the scale transform to the left edge so the bar grows right rather than expanding from the centre.

The @supports Wrapper

Scroll-driven animations are a progressive enhancement. Wrapping the whole thing in @supports (animation-timeline: scroll()) means browsers without support simply don't show the bar; a clean absence rather than a broken presence. No polyfill, no fallback JavaScript needed.

Animation Range

The animation-range property gives fine control over when within the scroll range the animation plays. entry 0% exit 0% means: start when the element enters the scroll container, finish when it exits. For a document-level bar, this maps to the full page scroll.

You can use this for scroll-triggered reveals too: set a range that covers just one section and the animation only plays when that section is in view.

Browser Support

Scroll-driven animations landed in Chrome 115 and Edge 115. Firefox shipped support in version 110 behind a flag, with full support in 132. Safari support arrived in 18.2. The @supports wrapper handles the gap cleanly in older browsers.

What This Replaces

The JavaScript equivalent requires a scroll event listener, a handler that reads scrollY, calculates a percentage against document.body.scrollHeight, and updates a style property, typically with a requestAnimationFrame wrapper for performance. That is eight to fifteen lines of JavaScript that this one CSS property replaces entirely.

Scroll to the bottom to see the bar complete.