← Text reveals

Glyph Rain Reveal

A column of random characters rains down into each letter’s place, scrambling as it falls. The columns slow as they land, each at its own moment, and leave the real letters behind.

Decoded

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-glyph-rain-reveal to a heading or short line of text.

Options

Add any of these attributes to the same element.

data-colorThe colour of the falling trail. Default #a855f7.
data-trailHow many characters trail above each one. Default 5.
data-durationHow long a column takes to fall. Default 1500 ms.
data-staggerThe random spread in when columns start. Default 900 ms.
data-loopRain in, hold, fade away and rain in again.
data-holdHow long the text stays before fading, when looping. Default 1800 ms.
data-delayWait before starting. Default 0 ms.

The trails fall from just above the text, so leave a little room over it.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Glyph Rain Reveal: a column of random characters falls into each letter's
    // place, slows down as it arrives and leaves the real letter behind.
    // Usage: <h1 data-glyph-rain-reveal>Decoded</h1>
    // Optional: data-color (the falling trail, default "#a855f7"), data-trail
    // (characters in each column, default 5), data-duration (ms for a column to
    // fall, default 1500), data-stagger (ms of random spread between columns,
    // default 900), data-loop (fade away and rain in again), data-hold (ms to
    // stay before fading, when looping), data-delay (ms before starting).
    var DEFAULTS = { color: '#a855f7', trail: 5, duration: 1500, stagger: 900, hold: 1800, delay: 0 };
    var GLYPHS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@*+=<>?/';

    var css =
      '.grr-on{clip-path:inset(-1.4em -0.3em -0.3em -0.3em)}' +
      '.grr-word{display:inline-block;white-space:nowrap}' +
      '.grr-ch{display:inline-block;position:relative;transition:color .6s ease}' +
      '.grr-hide{color:transparent!important}' +
      '.grr-col{position:absolute;left:50%;bottom:0;display:flex;flex-direction:column;align-items:center;' +
      'pointer-events:none;will-change:transform;' +
      '-webkit-mask-image:linear-gradient(to bottom,transparent,#000 70%);mask-image:linear-gradient(to bottom,transparent,#000 70%)}' +
      '.grr-col span{display:block;text-align:center}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function glyph() { return GLYPHS[(Math.random() * GLYPHS.length) | 0]; }

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

    // Builds the falling column for one letter: trail glyphs on top, the head
    // (which becomes the real letter) at the bottom.
    function column(ch, n, color) {
      var col = document.createElement('span');
      col.className = 'grr-col';
      col.setAttribute('aria-hidden', 'true');
      var h = ch.offsetHeight;
      var cells = [];
      for (var i = 0; i <= n; i++) {
        var g = document.createElement('span');
        g.style.height = h + 'px';
        g.style.lineHeight = h + 'px';
        g.textContent = glyph();
        if (i < n) {
          g.style.color = color;
          g.style.opacity = (0.25 + 0.6 * (i + 1) / n).toFixed(2);
        }
        col.appendChild(g);
        cells.push(g);
      }
      ch.appendChild(col);
      return { col: col, cells: cells, h: h, ch: ch, real: ch.firstChild.nodeValue, landed: false };
    }

    function rain(el, done) {
      var s = el._grr, d = el.dataset;
      var n = Math.max(1, +d.trail || DEFAULTS.trail);
      var color = d.color || DEFAULTS.color;
      var duration = +d.duration || DEFAULTS.duration;
      var spread = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var left = s.chars.length;
      var cols = s.chars.map(function (ch) {
        ch.classList.add('grr-hide');
        ch.style.opacity = '';
        ch.style.transform = '';
        return column(ch, n, color);
      });

      // Scramble the glyphs while they fall, slower as each column slows.
      var start = performance.now();
      (function flicker() {
        var live = false;
        cols.forEach(function (c) {
          if (c.landed) return;
          live = true;
          c.cells.forEach(function (g, i) {
            var settling = c.p > 0.7 && i === n;
            if (settling) g.textContent = c.real;
            else if (Math.random() < 0.9 - (c.p || 0) * 0.6) g.textContent = glyph();
          });
        });
        if (live) setTimeout(flicker, 60 + (performance.now() - start) / 40);
      })();

      cols.forEach(function (c) {
        c.p = 0;
        var dur = duration * (0.75 + Math.random() * 0.5);
        var from = -(n + 2 + Math.random() * 3) * c.h;
        c.col.style.transform = 'translate(-50%,' + from + 'px)';
        anime.animate(c, {
          p: [0, 1],
          duration: dur,
          delay: Math.random() * spread,
          ease: 'outCubic',
          onUpdate: function () {
            c.col.style.transform = 'translate(-50%,' + (from * (1 - c.p)).toFixed(1) + 'px)';
          },
          onComplete: function () {
            c.landed = true;
            // The real letter takes over from the head, flashing the trail colour.
            c.ch.style.transition = 'none';
            c.ch.style.color = color;
            c.ch.classList.remove('grr-hide');
            c.cells[n].textContent = '';
            void c.ch.offsetWidth;
            c.ch.style.transition = '';
            requestAnimationFrame(function () { c.ch.style.color = ''; });
            anime.animate(c.col, {
              opacity: [1, 0],
              duration: 500,
              ease: 'outQuad',
              onComplete: function () {
                c.col.remove();
                if (--left === 0 && done) done();
              }
            });
          }
        });
      });
    }

    function play(el) {
      var d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      rain(el, function () {
        if (!loop) return;
        setTimeout(function () {
          anime.animate(el._grr.chars, {
            opacity: [1, 0],
            y: ['0em', '0.4em'],
            duration: 500,
            ease: 'inQuad',
            delay: anime.stagger(30),
            onComplete: function () { setTimeout(function () { play(el); }, 300); }
          });
        }, +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); }, +el.dataset.delay || DEFAULTS.delay);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-glyph-rain-reveal]').forEach(function (el) {
        if (el._grr) return;
        el._grr = { chars: split(el) };
        if (reduced()) return;
        el._grr.chars.forEach(function (c) { c.classList.add('grr-hide'); });
        io.observe(el);
      });
    }

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

<!-- or, with green trails, looping: -->
<h1 data-glyph-rain-reveal data-color="#22c55e" data-loop>Decoded</h1>