← Text reveals

Random-order Reveal

Letters blink into place in a shuffled order, each one flashing a highlight colour as it lands, until the whole line has pieced itself together. The order is different every time.

Pieced together

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-random-order-reveal to any heading or text element.

Options

Add any of these attributes to the same element.

data-loopAppear, hold, vanish in a new order and appear in another.
data-holdHow long the text stays before vanishing, when looping. Default 1800 ms.
data-colorColour each letter flashes as it lands. Default #a855f7.
data-durationHow long each letter takes to settle. Default 700 ms.
data-staggerGap between letters. Default 45 ms.
data-delayWait before the first letter. Default 0 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Random-order Reveal: letters blink into place in a shuffled order, each
    // one flashing a highlight colour as it lands, until the whole line is there.
    // Usage: <h1 data-random-order-reveal>Pieced together</h1>
    // Optional: data-loop (letters vanish in a new order and reappear in
    // another), data-hold (ms to stay before vanishing), data-color (flash
    // colour; default #a855f7), data-duration (ms per letter), data-stagger (ms
    // between letters), data-delay (ms before starting).
    var DEFAULTS = { color: '#a855f7', duration: 700, stagger: 45, delay: 0, hold: 1800 };

    var css =
      '.ror-word{display:inline-block;white-space:nowrap}' +
      '.ror-char{display:inline-block;will-change:transform,opacity}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function split(el) {
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      el.setAttribute('aria-label', text);
      el.textContent = '';
      var chars = [];
      text.split(' ').forEach(function (word, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var w = document.createElement('span');
        w.className = 'ror-word';
        w.setAttribute('aria-hidden', 'true');
        Array.from(word).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'ror-char';
          s.textContent = c;
          w.appendChild(s);
          chars.push(s);
        });
        el.appendChild(w);
      });
      return chars;
    }

    // A fresh random order every time (Fisher-Yates).
    function shuffled(list) {
      var a = list.slice();
      for (var i = a.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var t = a[i]; a[i] = a[j]; a[j] = t;
      }
      return a;
    }

    function play(el, chars) {
      var d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var duration = +d.duration || DEFAULTS.duration;
      var stagger = +d.stagger || DEFAULTS.stagger;
      var hold = +d.hold || DEFAULTS.hold;
      var color = d.color || DEFAULTS.color;
      var base = getComputedStyle(el).color;

      function reveal() {
        // The stagger follows the order of the list, so shuffling it shuffles
        // the order the letters arrive in.
        anime.animate(shuffled(chars), {
          opacity: { from: 0, to: 1, duration: duration * 0.25, ease: 'linear' },
          scale: [1.45, 1],
          color: [color, base],
          duration: duration,
          ease: 'out(3)',
          delay: anime.stagger(stagger),
          onComplete: loop ? function () { setTimeout(vanish, hold); } : undefined
        });
      }
      function vanish() {
        anime.animate(shuffled(chars), {
          opacity: 0,
          scale: 0.8,
          duration: duration * 0.35,
          ease: 'in(2)',
          delay: anime.stagger(stagger * 0.6),
          onComplete: function () { setTimeout(reveal, 400); }
        });
      }
      setTimeout(reveal, +d.delay || DEFAULTS.delay);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target, e.target._rorChars);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-random-order-reveal]').forEach(function (el) {
        if (el._rorChars) return;
        var chars = split(el);
        el._rorChars = chars;
        if (reduced()) return;
        anime.utils.set(chars, { opacity: 0 });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-random-order-reveal>Pieced together</h1>

<!-- a yellow flash, looping: -->
<h1 data-random-order-reveal data-color="#facc15" data-loop>Pieced together</h1>