← Text reveals

Glitch Reveal

The text jitters into view through a burst of RGB-split glitches, torn slices and flicker, as if the signal were tuning in, then snaps clean and holds perfectly still.

Signal found

How to use it

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

Options

Add any of these attributes to the same element.

data-colorsThe two split colours, comma-separated. Default #ff2a6d,#05d9e8.
data-durationHow long the glitching lasts. Default 1000 ms.
data-loopGlitch in, hold, glitch out and in again.
data-holdHow long the text stays clean before glitching out, when looping. Default 2200 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 () {
    // Glitch Reveal: text jitters into view through a burst of RGB-split
    // glitches, torn slices and flicker, then snaps clean and holds still.
    // Usage: <h1 data-glitch-reveal>Signal found</h1>
    // Optional: data-colors (the two split colours, default "#ff2a6d,#05d9e8"),
    // data-duration (ms of glitching, default 1000), data-loop (glitch out, then
    // in again), data-hold (ms to stay clean before glitching out), data-delay
    // (ms before starting).
    var DEFAULTS = { colors: '#ff2a6d,#05d9e8', duration: 1000, hold: 2200, delay: 0 };

    var css =
      '.glr-on{position:relative;isolation:isolate}' +
      '.glr-main{display:block;position:relative;z-index:1}' +
      '.glr-ghost{position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var R = function (a, b) { return a + Math.random() * (b - a); };

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

    function setup(el) {
      var d = el.dataset, cs = getComputedStyle(el);
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      var colors = (d.colors || DEFAULTS.colors).split(',');
      el.setAttribute('aria-label', text);
      el.textContent = '';
      el.classList.add('glr-on');
      var ghosts = colors.slice(0, 2).map(function (c) {
        var g = document.createElement('span');
        g.className = 'glr-ghost';
        g.setAttribute('aria-hidden', 'true');
        g.textContent = text;
        g.style.color = c.trim();
        g.style.padding = cs.padding;
        el.appendChild(g);
        return g;
      });
      var main = document.createElement('span');
      main.className = 'glr-main';
      main.setAttribute('aria-hidden', 'true');
      main.textContent = text;
      el.appendChild(main);
      return { main: main, ghosts: ghosts, fs: parseFloat(cs.fontSize) };
    }

    // A random horizontal band, as an inset() clip.
    function band() {
      var top = R(0, 85);
      return 'inset(' + top.toFixed(0) + '% 0 ' + Math.max(0, 100 - top - R(8, 40)).toFixed(0) + '% 0)';
    }

    function clean(s) {
      s.main.style.cssText = '';
      s.ghosts.forEach(function (g) { g.style.opacity = 0; g.style.transform = ''; g.style.clipPath = ''; });
    }

    // One glitch frame. `q` runs 0 (all noise) to 1 (settled).
    function frame(s, q) {
      var amt = (1 - q) * s.fs;
      var show = Math.random() < 0.25 + q * 0.85;
      s.main.style.opacity = show ? (Math.random() < 0.2 ? 0.6 : 1) : 0;
      s.main.style.transform = 'translate(' + (R(-0.06, 0.06) * amt).toFixed(1) + 'px,0) skewX(' + R(-8, 8) * (1 - q) + 'deg)';
      s.main.style.clipPath = Math.random() < 0.45 * (1 - q) ? band() : '';
      s.ghosts.forEach(function (g, i) {
        var on = Math.random() < 0.9 - q * 0.4;
        g.style.opacity = on ? 0.9 : 0;
        var dir = i ? 1 : -1;
        g.style.transform = 'translate(' + (dir * R(0.03, 0.14) * amt).toFixed(1) + 'px,' + (R(-0.03, 0.03) * amt).toFixed(1) + 'px)';
        g.style.clipPath = Math.random() < 0.6 ? band() : '';
      });
    }

    // Runs glitch frames for `duration` ms at about 20 fps; `out` plays it
    // backwards, ending hidden.
    function burst(el, out, done) {
      var s = el._glr, d = el.dataset;
      var duration = +d.duration || DEFAULTS.duration;
      var start = performance.now();
      (function tick() {
        var q = Math.min(1, (performance.now() - start) / duration);
        if (q >= 1) {
          clean(s);
          if (out) s.main.style.opacity = 0;
          else {
            // A last little twitch as it snaps clean.
            s.main.style.textShadow = (-0.03 * s.fs) + 'px 0 ' + s.ghosts[0].style.color + ',' +
              (0.03 * s.fs) + 'px 0 ' + (s.ghosts[1] || s.ghosts[0]).style.color;
            setTimeout(function () { s.main.style.textShadow = ''; }, 70);
          }
          return done && done();
        }
        frame(s, out ? 1 - q : Math.pow(q, 1.4));
        setTimeout(tick, R(35, 75));
      })();
    }

    function play(el) {
      var d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      burst(el, false, function () {
        if (!loop) return;
        setTimeout(function () {
          burst(el, true, function () { setTimeout(function () { play(el); }, 500); });
        }, +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.3 });
      document.querySelectorAll('[data-glitch-reveal]').forEach(function (el) {
        if (el._glr) return;
        el._glr = setup(el);
        if (reduced()) return;
        el._glr.main.style.opacity = 0;
        io.observe(el);
      });
    }

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

<!-- or, with yellow and blue splits, looping: -->
<h1 data-glitch-reveal data-colors="#facc15,#3b82f6" data-loop>Signal found</h1>