Encodera

Thanks for reading!

Donate to the site — we work hard for you.

How to make a reading progress bar

Short answer

Tie the animation to the page scroll instead of time:

.progress {
  position: fixed; top: 0; left: 0; width: 100%; height: 4px;
  background: tomato; transform-origin: left;
  animation: grow linear;
  animation-timeline: scroll(root);
}
 
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

animation-timeline: scroll(root) replaces seconds with the scroll position: at the top of the page the animation is at 0%, at the bottom at 100%. The bar grows from scaleX(0) to full width. No JavaScript is needed.

A live example in a scrolling box

Without an argument scroll() uses the nearest scrolling parent. Scroll the text:

<style>
  .article { height: 160px; overflow: auto; font: 14px/1.6 system-ui, sans-serif; border: 1px solid #ddd; }
  .bar { position: sticky; top: 0; height: 4px; background: tomato; transform-origin: left;
    animation: grow linear; animation-timeline: scroll(); }
  @keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  .article p { margin: 12px; }
</style>
 
<div class="article">
  <div class="bar"></div>
  <p>Scroll down to fill the bar.</p>
  <p>CSS can tie an animation to the scroll position instead of time.</p>
  <p>The bar grows as you read and reaches full width at the end.</p>
  <p>No JavaScript is involved: the browser does all the work.</p>
  <p>The same trick works for the whole page with scroll(root).</p>
  <p>You have reached the end.</p>
</div>

position: sticky keeps the bar at the top of the box while scrolling.

A fallback

Scroll-driven animations are not in every browser yet. Where they are missing, animation-timeline is skipped, the animation without a duration ends instantly and the bar freezes at full width. The page does not break, but the indicator lies. So wrap the bar styles in @supports (animation-timeline: scroll()) — then older browsers simply do not show it. If you need the indicator everywhere, a few lines of JavaScript on the scroll event can move it.

Common mistake

Writing animation-timeline before animation. The animation shorthand resets animation-timeline to its default, and the animation runs on time again instead of scroll. Always put animation-timeline after animation.

Functions used in this recipe

  • animation-timeline — Ties a CSS animation to scrolling instead of time: the animation progresses as the page or an element is scrolled.
  • scroll() — Create anonymous scroll timelines for `animation-timeline`: `scroll()` tracks a scroll container, `view()` tracks the element's own trip through the visible area.
  • @keyframes — Describes the steps of an animation that is then started with the `animation` property.
  • position — How an element is positioned: in the normal flow or by offsets.
  • @supports — Applies rules only if the browser supports a given property, value or selector.

Thanks for reading!

Donate to the site — we work hard for you.