Snippets /

Scroll progress bar

Reading indicator driven by animation-timeline: scroll() — zero JavaScript.

Scroll me to see the bar fill

The progress bar above is driven entirely by CSS. As you scroll this container, the animation-timeline: scroll() ties the bar's width animation to the scroll position.

No JavaScript reads scrollY. No requestAnimationFrame loop. The browser composites the animation on the GPU and updates it in sync with scroll, which is more efficient than a JS equivalent.

The gradient bar goes from the site accent color to a purple, making the fill visually satisfying. You could also use scaleX() instead of width for better performance on long pages.

This pattern works on any scrollable container — not just the document root. You can scope it to an article body, a sidebar, or a modal with overflow-y: auto.

Widely supported
animationno-js

Quick implementation

/* 1. Sticky wrapper at the top of your article/page */
.progress-wrap {
  position: sticky; top: 0;
  height: 3px;
  background: oklch(0 0 0 / 0.08);
}

/* 2. The bar — width animated by scroll */
.progress-bar {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, oklch(0.52 0.22 265), oklch(0.55 0.2 300));
  animation: fill-progress linear;
  animation-timeline: scroll(); /* document root */
  /* Use scroll(self) if scoping to a scroll container */
}

@keyframes fill-progress {
  to { width: 100%; }
}

/* Optional: reduce-motion fallback */
@media (prefers-reduced-motion: reduce) {
  .progress-bar { animation: none; width: 100%; opacity: 0.2; }
}

Prompt this to your LLM

Includes scroll-container scoping and a React variant.

You are a senior frontend engineer specializing in modern CSS.

Goal: Build a reading-progress bar that fills as the user scrolls — using CSS Scroll-Driven Animations, no JavaScript.

Technical constraints:
- Use animation-timeline: scroll() to tie a keyframe animation to scroll position.
- The keyframe should animate width (or scaleX on a transform-origin: left bar) from 0% to 100%.
- The bar should be position: sticky at the top of the scrollable content so it stays visible.
- Use a gradient fill (two oklch() colors) for visual interest.

Variants to cover:
A) Scoped to the document: animation-timeline: scroll() (default, uses root scroller).
B) Scoped to a container: add overflow-y: scroll to a wrapper and use animation-timeline: scroll(self).

Edge cases:
- Add @media (prefers-reduced-motion: reduce) { .progress-bar { animation: none; width: 100%; opacity: 0.2; } }.
- Mention that scaleX is more performant than width on very long pages (avoids layout recalculation).
- Note browser support: widely supported in Chrome/Edge; Safari added it in 18+.

Return a self-contained HTML + CSS example with both variants clearly labeled.

Why this matters in 2026

Progress indicators are one of the most common JS micro-patterns: listen to scroll, read scrollY / scrollHeight, update a div's width. Scroll-driven animations make this a single keyframe. The browser handles the math and composites it off the main thread.

Unlike JS listeners that run every few milliseconds on scroll, animation-timeline: scroll() is integrated into the rendering pipeline and doesn't block JavaScript. It's also easier to maintain — the "logic" is just a keyframe.

The logic

animation-timeline: scroll() sets the scroll timeline to the nearest scroll ancestor (or the document root). The animation plays forward as the user scrolls down and backward as they scroll up. scroll(self) scopes it to the element's own scroll container instead of the document — useful for progress bars inside scrollable panels or modals.

Using scaleX(0) → scaleX(1) with transform-origin: left instead of width avoids layout calculations and is faster on long pages.

Accessibility & performance

The bar is decorative — don't add ARIA to it. Respect prefers-reduced-motion: reduce: a simple animation: none with a static low-opacity bar gives a subtle visual cue without motion. The CSS-only approach means no event listeners and no main-thread work during scroll.