Scroll progress rule
A hairline that fills across the top of the viewport in proportion to reading progress.
Grade: ARuntimes: cssStatus: code-only
Documented, no live demo yet — the code below is complete.
Intent
On a page that is deliberately long and light on chrome, a progress rule is the one piece of orientation that costs almost nothing. It answers 'how much is left' without adding a scrollbar-sized visual element.
Implementation
The `@supports` block is the whole implementation on modern browsers; the script only runs where scroll-driven animations are unavailable.
Dependencies: none
<div class="progress" aria-hidden="true"><span class="progress__fill"></span></div>
<style>
.progress {
position: fixed;
inset-block-start: 0;
inset-inline: 0;
height: 2px;
z-index: var(--z-nav, 8000);
pointer-events: none;
}
.progress__fill {
display: block;
height: 100%;
background: var(--accent, #3d2fe8);
transform: scaleX(var(--p, 0));
transform-origin: 0 50%;
}
@supports (animation-timeline: scroll()) {
.progress__fill {
animation: progress-grow linear both;
animation-timeline: scroll(root block);
}
@keyframes progress-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
}
</style>
<script type="module">
if (!CSS.supports("animation-timeline: scroll()")) {
const fill = document.querySelector(".progress__fill");
let pending = false;
const update = () => {
const max = document.documentElement.scrollHeight - innerHeight;
fill.style.setProperty("--p", String(max > 0 ? scrollY / max : 0));
pending = false;
};
addEventListener(
"scroll",
() => {
if (pending) return;
pending = true;
requestAnimationFrame(update);
},
{ passive: true },
);
update();
}
</script>Use it when
- Long-form articles, case studies, single-page sites.
Avoid it when
- Short pages, where the bar is either always full or always empty.
- Pages with pinned horizontal sections, where 'progress' no longer maps to reading position.
Accessibility
- Decorative — mark it `aria-hidden`. Screen readers already report position.
- If you expose it as a `progressbar` role instead, it must have an accessible name and live value, which is rarely worth it.
- Reduced motion does not apply: this tracks input directly rather than animating on its own.
Performance
- `animation-timeline: scroll()` runs off the main thread. Prefer it and treat the rAF version as the fallback.
- Only `transform` is animated, so there is no layout or paint per frame.
Knobs
- thickness
- Rule height. Default: 2px
- colour
- Fill colour. Default: var(--accent)
Composition
Conflicts with
Instruction for Claude Code
Add a scroll progress rule: a fixed 2px full-width aria-hidden bar at the top with a scaleX fill and transform-origin left. Implement it with animation-timeline: scroll(root block) inside an @supports block so it runs off the main thread, and only register a rAF-throttled passive scroll listener when CSS.supports reports the timeline is unavailable. Animate transform only.