Scroll Progress Indicator
A bar (typically at the top of the page) that fills horizontally as the user scrolls down, visually communicating how far through a long document the reader has progressed.
Plain English
Long articles and documentation pages have a time-to-read uncertainty problem: the user does not know if they are 20% or 80% through without looking at the scrollbar, which is easy to miss or hidden on modern OSes. A scroll progress indicator solves this with a prominent colored bar that fills from left to right as you scroll down. It is a small affordance that reduces the psychological cost of committing to reading a long piece — you can see the finish line. It also doubles as a subtle branding element when colored with your primary accent. The native CSS version using scroll-driven animations is now just a few lines of code and requires no JavaScript at all.
Technical
JavaScript approach: on `scroll` event (throttled or passive), compute `scrollPercent = window.scrollY / (document.body.scrollHeight - window.innerHeight) * 100` and set `progressBar.style.width = scrollPercent + "%"`. Position with `position: fixed; top: 0; left: 0; height: 3–4px; z-index: 9999`. CSS-only scroll-driven animation (Chrome 115+, Safari 18+): `@keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } } .progress { position: fixed; top: 0; left: 0; right: 0; height: 4px; transform-origin: left; animation: progress auto linear; animation-timeline: scroll(); }`. The `animation-timeline: scroll()` ties the animation playback to the document scroll position — no JS needed. For browser support, provide the JS fallback inside `@supports not (animation-timeline: scroll())`. Avoid `overflow: hidden` on the `<html>` element — it breaks `scroll()` timeline. Use `will-change: transform` on the bar element for GPU compositing.
Live Demo
Scroll Progress Indicator
A thin bar at the top of the page fills as the user scrolls. Drag the slider to simulate scroll progress.
Design Systems Guide
0% readModern CSS-only approach:
.progress-bar {
position: fixed; top: 0; left: 0;
height: 3px;
background: var(--accent);
transform-origin: left;
animation: progress-grow linear;
animation-timeline: scroll(root);
}
With CSS scroll-driven animations, no JavaScript is needed — the browser handles it natively.
Usage
✓ Good usage
A blog article page with a 3px primary-colored progress bar fixed to the top, implemented via CSS scroll-driven animation with a JS fallback — it appears only on article pages, not on the homepage or app screens where scroll position is irrelevant.
✗ Bad usage
Adding a scroll progress indicator to every page, including a login screen or a dashboard with multiple scroll containers — on short pages it snaps instantly to 100%, and on pages with inner scroll areas it tracks the wrong container.
Recommended values
- Height: 3–4px (thin but visible)
- Position: fixed, top: 0, left: 0, z-index: 9999
- Color: primary brand accent or a gradient
- JS: passive scroll listener + requestAnimationFrame throttle
- CSS-only: animation-timeline: scroll() with scaleX transform
- Only show on long content pages (> 2× viewport height)
Common mistakes
- Using `document.documentElement.scrollTop` and `document.body.scrollHeight` inconsistently across browsers — normalize to `window.scrollY` and `document.documentElement.scrollHeight - window.innerHeight`.
- Not accounting for the viewport height in the denominator — `scrollHeight / totalHeight` stops at ~80% because the viewport occupies the remaining 20%.
- Attaching the scroll listener without `{ passive: true }` — blocks the main thread and causes scroll jank.
- Showing the indicator on pages shorter than the viewport — the bar either stays at 0% or jumps to 100% immediately, both of which look broken.
AI Prompt Example
Copy this into Claude, Cursor, Bolt, or v0.
Add a scroll progress indicator to all blog/article pages. Use CSS scroll-driven animation as the primary implementation: a fixed 4px div at `top: 0` with `transform-origin: left`, `animation: scaleX linear`, and `animation-timeline: scroll()`. For browsers that do not support scroll-driven animations, fall back to a JS passive scroll listener that computes `window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)` and sets the bar width. Color the bar with the primary brand accent. Only mount the component on routes under `/blog`.