← Scroll

Scroll-scrubbed Text

The words of a paragraph light up one by one as you scroll through it, and dim again if you scroll back. It’s tied to the scroll position, so the reader sets the pace.

Scroll inside ↓

Every word lights up as you scroll, so the reader sets the pace and nothing is missed.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-scroll-scrubbed-text to a paragraph or heading.

Options

Add any of these attributes to the same element.

data-dimOpacity of words not reached yet. Default 0.18.
data-colorColour words turn as they light up.

Works inside scrolling boxes too: it follows the nearest scrolling parent, or the page.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Scroll-scrubbed Text: the words of a paragraph light up one by one as you
    // scroll through it, and dim again if you scroll back. Tied to the scroll
    // position, so the reader sets the pace.
    // Usage: <p data-scroll-scrubbed-text>Every word lights up as you read…</p>
    // Optional: data-dim (opacity of words not reached yet, default 0.18),
    // data-color (colour words turn as they light up).
    // Works inside scrolling boxes too: it follows the nearest scrolling parent.
    var DEFAULTS = { dim: 0.18 };

    function split(el) {
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      el.setAttribute('aria-label', text);
      el.textContent = '';
      return text.split(' ').map(function (w, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var s = document.createElement('span');
        s.textContent = w;
        s.setAttribute('aria-hidden', 'true');
        el.appendChild(s);
        return s;
      });
    }

    function scroller(el) {
      for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
        var o = getComputedStyle(p).overflowY;
        if ((o === 'auto' || o === 'scroll') && p.scrollHeight > p.clientHeight) return p;
      }
      return null;
    }

    function setup(el) {
      var d = el.dataset;
      var dim = d.dim !== undefined ? +d.dim : DEFAULTS.dim;
      var words = split(el);
      var box = scroller(el);
      var color = d.color;
      var base = getComputedStyle(el).color;
      var last = -1;

      function update() {
        var r = el.getBoundingClientRect();
        var top = 0, h = window.innerHeight;
        if (box) { var b = box.getBoundingClientRect(); top = b.top; h = box.clientHeight; }
        // 0 when the paragraph's top reaches 85% down the view, 1 when its bottom
        // passes 45% down: the words light up while it crosses the middle.
        var start = top + h * 0.85, end = top + h * 0.45;
        var p = (start - r.top) / (start - end + r.height);
        p = Math.max(0, Math.min(1, p));
        var lit = p * words.length;
        if (Math.abs(lit - last) < 0.01) return;
        last = lit;
        words.forEach(function (w, i) {
          var t = Math.max(0, Math.min(1, lit - i));
          w.style.opacity = dim + (1 - dim) * t;
          if (color) w.style.color = t > 0.5 ? color : base;
        });
      }

      var queued = false;
      function onScroll() {
        if (queued) return;
        queued = true;
        requestAnimationFrame(function () { queued = false; update(); });
      }
      (box || window).addEventListener('scroll', onScroll, { passive: true });
      window.addEventListener('resize', onScroll);
      update();
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-scroll-scrubbed-text]').forEach(function (el) {
        if (el._sst) return;
        el._sst = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p data-scroll-scrubbed-text>
  We build tools that get out of your way, so you can
  spend your time on the work that matters.
</p>