← Scroll

Scroll Progress Bar

A thin bar along the top of the screen fills up as the reader scrolls, so they always know how much is left. Track the whole page, one article or a scrolling box.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-scroll-progress-bar></div> anywhere in the page. It fixes itself to the top of the screen.

Options

Add any of these attributes to the same element.

data-targetA selector: measure progress through one element (e.g. #article) or a scrollable box.
data-colorAny CSS colour or gradient. Default purple gradient.
data-heightThickness in px. Default 4.
data-position="inline"Stay where you put it instead of fixing to the top of the screen.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Scroll Progress Bar: a thin bar along the top of the screen fills up as the
    // reader scrolls, so they always know how much is left.
    // Usage: <div data-scroll-progress-bar></div>   (anywhere in the page)
    // Optional: data-target="#article" (measure progress through one element, or
    // through a scrollable box), data-color (default purple gradient), data-height
    // (px, default 4), data-position="inline" (stay where you put it instead of
    // fixing to the top of the screen).
    var DEFAULTS = { color: 'linear-gradient(90deg, #a855f7, #6a0dad)', height: 4 };

    function setup(el) {
      var d = el.dataset;
      var inline = d.position === 'inline';
      el.setAttribute('role', 'progressbar');
      el.setAttribute('aria-label', 'Reading progress');
      el.setAttribute('aria-valuemin', '0');
      el.setAttribute('aria-valuemax', '100');
      el.style.cssText += (inline ? 'position:relative;' : 'position:fixed;top:0;left:0;right:0;z-index:2147482000;') +
        'height:' + (+d.height || DEFAULTS.height) + 'px;pointer-events:none';
      var fill = document.createElement('div');
      fill.style.cssText = 'height:100%;background:' + (d.color || DEFAULTS.color) + ';transform-origin:0 50%;transform:scaleX(0);border-radius:0 2px 2px 0';
      el.appendChild(fill);

      var target = d.target ? document.querySelector(d.target) : null;
      var box = null;
      if (target) {
        var oy = getComputedStyle(target).overflowY;
        if (oy === 'auto' || oy === 'scroll') box = target;
      }
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

      function progress() {
        if (box) return box.scrollTop / Math.max(1, box.scrollHeight - box.clientHeight);
        if (target) {
          var r = target.getBoundingClientRect();
          return -r.top / Math.max(1, r.height - window.innerHeight);
        }
        var max = document.documentElement.scrollHeight - window.innerHeight;
        return window.scrollY / Math.max(1, max);
      }
      function update() {
        var p = Math.min(1, Math.max(0, progress()));
        el.setAttribute('aria-valuenow', Math.round(p * 100));
        if (reduced) { fill.style.transform = 'scaleX(' + p + ')'; return; }
        // A short ease smooths out jumpy wheel scrolling.
        anime.animate(fill, { scaleX: p, duration: 250, ease: 'out(3)' });
      }
      (box || window).addEventListener('scroll', update, { passive: true });
      window.addEventListener('resize', update);
      update();
    }

    function init() {
      document.querySelectorAll('[data-scroll-progress-bar]').forEach(function (el) {
        if (el._spb) return;
        el._spb = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-scroll-progress-bar></div>

<!-- progress through the article only: -->
<div data-scroll-progress-bar data-target="#article"></div>