CSS Scroll Progress Bar — Reading Indicator

Add a sticky progress bar that fills as users scroll your page, using CSS scroll-timeline or JavaScript. Live demo, configurable, copy the code.

CSS-Only Progress Bar with scroll-timeline

A scrollable container with scroll-timeline-name: --pb-tl drives a sticky bar's fill animation. No JavaScript; the bar's width tracks scroll position via animation-timeline: --pb-tl. Scroll inside the demo to see it fill.

Introduction

Scroll down to see the progress bar fill. It uses CSS scroll-timeline-name to link the bar's animation to this container's scroll position, zero JavaScript required.

The progress bar is sticky at the top and always visible as you scroll through the content below.

How It Works

The scroll container has scroll-timeline-name: --pb-tl. The fill element references it with animation-timeline: --pb-tl and animates width from 0% to 100% in keyframes.

Because the animation is driven by scroll position rather than elapsed time, the bar moves in perfect sync with the user's scrolling, no JavaScript timing or requestAnimationFrame needed.

Named vs Anonymous Timelines

A named timeline (scroll-timeline-name: --my-tl) lets any descendant reference it explicitly. An anonymous timeline (animation-timeline: scroll()) automatically uses the nearest scrollable ancestor. Named timelines are better when you have nested scroll containers.

Configuration

Use the controls on the right to change the bar color, height, and position. The color and height updates use CSS custom properties for instant feedback.

Try a gradient color for a more vibrant look, or increase the bar height to make the indicator more prominent on content-heavy pages.

The Sticky Positioning

The progress bar uses position: sticky so it stays pinned to the top (or bottom) of the scroll container as the user scrolls. Combined with a high z-index, it floats above the page content without affecting layout.

Browser Support

Works in Chrome 115+, Edge 115+, and Safari 26+. Firefox does not support scroll-timeline by default. For Firefox users, use the JavaScript approach in Demo 2 below, or detect support with CSS.supports().

Keyframes Explained

The @keyframes block animates width from 0% to 100%. With animation-fill-mode: both, the bar starts at 0% when you're at the top and stays at 100% when you reach the bottom. The linear timing function ensures a proportional fill.

Production Use

For a page-wide progress bar, put scroll-timeline-name: --page-tl on html and reference it from a fixed/sticky bar at the top of the page. This approach requires no container scoping and works across the entire document scroll.

You can also combine it with color transitions in the keyframes to shift the bar color as the user reads further down the page.

2px16px

JavaScript Progress Bar

Listens to the scroll event on the viewport container. Calculates progress as scrollTop / (scrollHeight - clientHeight) * 100 and updates the bar's width directly. Works in all browsers including Firefox.

JavaScript Progress Bar

This progress bar uses a scroll event listener on the container. No CSS scroll-timeline needed. Works in all modern browsers including Firefox.

Universal Browser Support

Chrome, Firefox, Safari, Edge: all modern browsers support the scroll event and the scrollTop / scrollHeight calculation. This makes JavaScript the safest cross-browser approach for progress indicators.

The Scroll Calculation

The formula is straightforward: scrollTop / (scrollHeight - clientHeight) * 100. This gives a percentage from 0 at the top to 100 at the bottom. The result is clamped to Math.min(100, pct) to prevent overflow from elastic scrolling on touch devices.

Performance

The scroll handler runs on the main thread. For smooth performance, the calculation is lightweight, just a division and a multiplication. For heavier updates, wrap the handler in requestAnimationFrame to batch DOM writes.

You can also use a passive: true event listener option to tell the browser you won't call preventDefault(), which enables smoother scrolling on mobile.

Page-Wide Version

For a page-wide bar, listen to window scroll and use window.scrollY / (document.body.scrollHeight - window.innerHeight) * 100. Place the bar element with position: fixed; top: 0; to keep it visible at all times.

Transition Smoothing

Adding a short CSS transition: width .05s linear on the fill element smooths out jitter from rapid scroll events. Keep the duration short; anything above 100ms will feel laggy and disconnected from the scroll position.

Customization

Change the color and height using the controls on the right. The updates happen instantly via inline style changes. You can also add gradient backgrounds, rounded corners, or even a glow effect with box-shadow on the fill element.

When to Use JavaScript

Choose JavaScript when you need Firefox support, want custom easing curves, need to trigger side effects at certain scroll thresholds, or want to combine the progress value with other UI updates like section highlighting or table-of-contents tracking.

2px16px

Section Progress Bar with fullPage.js

On fullscreen section-based sites, a progress bar can't use scrollTop. Instead, it tracks which section you're on, e.g. section 2 of 5 = 40%. Scroll inside the preview to see it in action.

Loading preview...
Get fullPage.js →

Scroll inside the preview to see the progress bar track fullscreen sections

How It Works

The CSS approach defines a named Scroll Progress Timeline on the scroll container. Any element within (or referencing) that container can then animate based on the container's scroll position.

/* 1. Give the scroll container a named timeline */
.article {
  overflow-y: scroll;
  scroll-timeline-name: --article-tl;
  scroll-timeline-axis: block;
  position: relative;
}

/* 2. Sticky bar sits at the top */
.progress-bar {
  position: sticky;
  top: 0;
  height: 4px;
  background: rgba(255,255,255,.1);
  z-index: 100;
}

/* 3. Fill animates from 0% to 100% */
.progress-bar-fill {
  height: 100%;
  background: #3B82F6;
  animation: fillBar linear both;
  animation-timeline: --article-tl;
}
@keyframes fillBar {
  from { width: 0%; }
  to   { width: 100%; }
}

/* ── Page-wide version ── */
html { scroll-timeline-name: --page-tl; }
.page-progress { animation-timeline: --page-tl; }

CSS vs JavaScript: Which to Use?

CSS scroll-timelineJavaScript (scroll event)
Browser supportChrome, Edge, Safari 26+ onlyAll browsers
Performance threadCompositor (off main thread)Main thread
Code complexityCSS only, ~15 lines~10 lines JS
Scroll direction reversalAutomaticAutomatic
Custom easinglinear onlyAny calculation
Multiple containersNamed timelinesMultiple listeners

Use CSS for new projects targeting modern browsers. Use JavaScript if you need Firefox support or more control over the easing and calculation logic.

FAQ

How do I add a reading progress bar in CSS?
Add scroll-timeline-name: --tl to your scroll container (html for page-wide). Create a sticky bar at the top with a fill element that has animation: fillBar linear both; animation-timeline: --tl;. The keyframes animate width from 0% to 100%.
What is scroll-timeline-name?
scroll-timeline-name creates a named Scroll Progress Timeline on a scroll container. Descendants reference it via animation-timeline: --your-name, linking their animation progress directly to the container's scroll position.
How do I make a scroll progress indicator with JavaScript?
Listen to the scroll event on your container: container.addEventListener('scroll', () => { const pct = container.scrollTop / (container.scrollHeight - container.clientHeight) * 100; bar.style.width = pct + '%'; });. For page-wide, use window.scrollY and document.body.scrollHeight - window.innerHeight.
Does the CSS progress bar work in Firefox?
No. scroll-timeline is not enabled in Firefox by default. Use the JavaScript approach for Firefox support, or use CSS.supports('animation-timeline', 'scroll()') to detect support and apply a JavaScript fallback when needed.

Make Your Website Stand Out with Fullscreen Effects

Add scroll transitions, parallax and animations to your site with fullPage.js. One component, 80+ effects.

Brandire
New World of Work
OW Consulting

Powering fullscreen design for Google, Sony, BBC, eBay & more

Get fullPage.js
+ Suggest Effect