Rejouice — loading0

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

html-css-js
<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

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.