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.