← Text reveals

Redaction Reveal

Every word starts hidden under a black redaction bar, like a censored document. One by one, in a shuffled order, the bars slide away to uncover the words.

Declassified at last

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-redaction-reveal to any heading or paragraph.

Options

Add any of these attributes to the same element.

data-colorThe bars. Default #111111.
data-orderrandom (default) or sequence, in reading order.
data-durationHow long each bar takes to slide away. Default 650 ms.
data-staggerGap between bars. Default 160 ms.
data-loopUncover, hold, cover up again and uncover.
data-holdHow long the words stay uncovered, when looping. Default 1800 ms.
data-delayWait before starting. Default 0 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Redaction Reveal: every word starts behind a black redaction bar, and the
    // bars slide away one after another to uncover the text.
    // Usage: <p data-redaction-reveal>Declassified at last</p>
    // Optional: data-color (the bars, default "#111111"), data-order (random or
    // sequence, default random), data-duration (ms per bar, default 650),
    // data-stagger (ms between bars, default 160), data-loop (bars slide back
    // over and away again), data-hold (ms to stay uncovered, when looping),
    // data-delay (ms before starting).
    var DEFAULTS = { color: '#111111', order: 'random', duration: 650, stagger: 160, hold: 1800, delay: 0 };

    var css =
      '.rdr-w{position:relative;display:inline-block;white-space:nowrap}' +
      '.rdr-bar{position:absolute;left:-.06em;right:-.06em;top:.08em;bottom:.04em;border-radius:.04em;' +
      'transform-origin:100% 50%;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() {
      return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    }

    function setup(el) {
      var d = el.dataset;
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      el.setAttribute('aria-label', text);
      el.textContent = '';
      var bars = [];
      text.split(' ').forEach(function (word, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var w = document.createElement('span');
        w.className = 'rdr-w';
        w.setAttribute('aria-hidden', 'true');
        w.textContent = word;
        var b = document.createElement('span');
        b.className = 'rdr-bar';
        b.style.background = d.color || DEFAULTS.color;
        w.appendChild(b);
        el.appendChild(w);
        bars.push(b);
      });
      return bars;
    }

    // Delay for each bar: in reading order, or shuffled.
    function delays(n, stagger, random) {
      var order = [];
      for (var i = 0; i < n; i++) order.push(i);
      if (random) {
        for (var j = n - 1; j > 0; j--) {
          var k = (Math.random() * (j + 1)) | 0, t = order[j];
          order[j] = order[k]; order[k] = t;
        }
      }
      var out = [];
      order.forEach(function (bar, slot) { out[bar] = slot * stagger; });
      return out;
    }

    function slide(el, uncover, done) {
      var d = el.dataset, bars = el._rdr;
      var duration = +d.duration || DEFAULTS.duration;
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var wait = delays(bars.length, uncover ? stagger : stagger * 0.4, (d.order || DEFAULTS.order) === 'random');
      var left = bars.length;
      bars.forEach(function (b, i) {
        // Bars leave off the right and come back in from the left.
        b.style.transformOrigin = uncover ? '100% 50%' : '0% 50%';
        anime.animate(b, {
          scaleX: uncover ? [1, 0] : [0, 1],
          duration: uncover ? duration : duration * 0.6,
          delay: wait[i],
          ease: uncover ? 'inOutQuart' : 'outQuart',
          onComplete: function () { if (--left === 0 && done) done(); }
        });
      });
    }

    function play(el) {
      var d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      slide(el, true, function () {
        if (!loop) return;
        setTimeout(function () {
          slide(el, false, function () { setTimeout(function () { play(el); }, 600); });
        }, +d.hold || DEFAULTS.hold);
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          var el = e.target;
          setTimeout(function () { play(el); }, 300 + (+el.dataset.delay || DEFAULTS.delay));
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-redaction-reveal]').forEach(function (el) {
        if (el._rdr) return;
        el._rdr = setup(el);
        if (reduced()) {
          el._rdr.forEach(function (b) { b.style.transform = 'scaleX(0)'; });
          return;
        }
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p data-redaction-reveal>The meeting took place on Tuesday.</p>

<!-- or, in reading order: -->
<h1 data-redaction-reveal data-order="sequence">Declassified at last</h1>