← Page transitions

Glitch Transition

A burst of digital glitches covers the cut: slices of the picture tear sideways, the colour channels split apart and blocks of noise flicker while the old view stutters into the new one. As a page transition the page jolts under bands of inverted colour until the screen cuts out, and the next page glitches back in. This page uses it, so try any link.

How to use it

  1. Add the snippet to every page of your site.
  2. Add data-glitch-transition to each page’s <body>.

Options

Add any of these attributes to the same element.

data-glitch-transition="#hex"The colour the screen cuts to between pages. Default #0b0b12.
data-intensityHow hard it glitches, from 0 to 1. Default 1.
data-durationTime for each half of a page change. Default 500 ms (750 ms for a whole slideshow change).
data-intervalContained mode only: time each slide stays. Default 2200 ms.

Links to other sites, new tabs, downloads and #anchors are left alone, and if anything goes wrong the link still opens. Coming back with the Back button glitches the page in again. Put data-glitch-transition on any other element with two or more children and the torn slices carry real pieces of both views, like the demo above.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Glitch Transition: a burst of digital glitches covers the cut. Slices of
    // the picture tear sideways, the colour channels split apart and blocks of
    // noise flicker while the old view stutters into the new one.
    // Usage: <body data-glitch-transition>   (add the snippet to every page)
    // Optional: data-glitch-transition="#hex" (the colour the screen glitches
    // to between pages, default #0b0b12), data-duration (ms for each half on a
    // page, default 500; for a whole slideshow change, default 750),
    // data-intensity (0 to 1, how hard the picture tears, default 1).
    // Contained mode: put data-glitch-transition on any other element with two
    // or more children. Every data-interval ms (default 2200) it glitches from
    // one child to the next.
    var DEFAULTS = { color: '#0b0b12', duration: 500, slideDuration: 750, interval: 2200, intensity: 1 };
    var KEY = 'animacious-glitch';
    var NOISE = ['#00f0ff', '#ff2bd6', '#ffffff', '#000000', '#39ff14'];

    var css =
      'html.gtr-pre body{opacity:0}' +
      '.gtr-layer{position:fixed;inset:0;z-index:2147483000;pointer-events:none;overflow:hidden}' +
      '.gtr-layer>i{position:absolute;left:0;width:100%;display:none}' +
      '.gtr-layer>.gtr-cover{inset:0;height:100%}' +
      '.gtr-contained{position:relative;overflow:hidden;display:grid}' +
      '.gtr-contained>*{grid-area:1/1}' +
      '.gtr-contained>.gtr-fx{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}' +
      '.gtr-fx>div,.gtr-fx>i{position:absolute;display:none;overflow:hidden}' +
      '.gtr-fx .gtr-copy{position:absolute;left:0;top:0;margin:0;max-width:none}' +
      '.gtr-fx>.gtr-rgb{inset:0;background:#000}' +
      '.gtr-rgb>.gtr-copy+.gtr-copy{mix-blend-mode:screen}' +
      '.gtr-fx>.gtr-slice{left:0;width:100%}' +
      '.gtr-fx>.gtr-lines{inset:0;background:repeating-linear-gradient(0deg,rgba(0,0,0,.28) 0 1px,transparent 1px 3px)}' +
      '.gtr-layer i.gtr-lines{inset:0;height:100%;background:repeating-linear-gradient(0deg,rgba(0,0,0,.3) 0 1px,transparent 1px 3px)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Two colour filters for the channel split: red only, and green and blue only.
    var NS = 'http://www.w3.org/2000/svg';
    var defs = document.createElementNS(NS, 'svg');
    defs.setAttribute('aria-hidden', 'true');
    defs.setAttribute('width', '0');
    defs.setAttribute('height', '0');
    defs.style.position = 'absolute';
    defs.innerHTML =
      '<filter id="gtr-red" color-interpolation-filters="sRGB"><feColorMatrix values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/></filter>' +
      '<filter id="gtr-cyan" color-interpolation-filters="sRGB"><feColorMatrix values="0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/></filter>';

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

    var arriving = false;
    try { arriving = sessionStorage.getItem(KEY) === '1'; sessionStorage.removeItem(KEY); } catch (e) {}
    function unhide() { document.documentElement.classList.remove('gtr-pre'); }
    if (arriving && !reduced()) {
      document.documentElement.classList.add('gtr-pre');
      setTimeout(unhide, 3000);
    }

    function rand(a, b) { return a + Math.random() * (b - a); }
    function pick(list) { return list[Math.floor(Math.random() * list.length)]; }

    // Runs paint(t) with t from 0 to 1, but only every `step` ms: glitches jump
    // from one broken frame to the next rather than gliding.
    function stepper(duration, step, paint, done) {
      var s = { t: 0 }, last = -1;
      paint(0);
      return anime.animate(s, {
        t: 1, duration: duration, ease: 'linear',
        onUpdate: function () {
          var k = Math.floor(s.t * duration / step);
          if (k !== last) { last = k; paint(s.t); }
        },
        onComplete: function () { if (done) done(); }
      });
    }

    // How hard it glitches at time t: quickly up, a hold, and down.
    function envelope(t) {
      return Math.min(1, t / 0.25, (1 - t) / 0.3);
    }

    function linkFor(e) {
      var a = e.target.closest && e.target.closest('a[href]');
      if (!a || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return null;
      if ((a.target && a.target !== '_self') || a.hasAttribute('download')) return null;
      var url = new URL(a.href, location.href);
      if (url.origin !== location.origin) return null;
      if (url.pathname === location.pathname && url.search === location.search && url.hash) return null;
      return url;
    }

    // ---------- Page mode: bars of inverted, colour-shifted page and a stuttering cover ----------

    function pageMode(body) {
      var duration = +body.dataset.duration || DEFAULTS.duration;
      var power = Math.max(0, Math.min(1, body.dataset.intensity === undefined ? DEFAULTS.intensity : +body.dataset.intensity));
      var color = body.getAttribute('data-glitch-transition') || DEFAULTS.color;
      var layer = document.createElement('div');
      layer.className = 'gtr-layer';
      layer.setAttribute('aria-hidden', 'true');
      var cover = document.createElement('i');
      cover.className = 'gtr-cover';
      cover.style.background = color;
      layer.appendChild(cover);
      var bars = [];
      for (var n = 0; n < 9; n++) bars.push(layer.appendChild(document.createElement('i')));
      var lines = layer.appendChild(document.createElement('i'));
      lines.className = 'gtr-lines';
      layer.style.display = 'none';
      // On <html>, not <body>: the body is shaken with a transform, which would
      // otherwise carry the fixed layer along with it.
      document.documentElement.appendChild(layer);
      var anim, trip = 0;

      function paint(t, out) {
        var k = out ? t : 1 - t;                 // 0: the page, 1: covered
        var g = envelope(t) * power;
        cover.style.display = Math.random() < k * k * 1.15 ? 'block' : 'none';
        lines.style.display = g > 0.15 ? 'block' : 'none';
        bars.forEach(function (b) {
          if (Math.random() > g * 0.85) { b.style.display = 'none'; return; }
          var fx = pick(['invert(1)', 'hue-rotate(' + Math.round(rand(60, 300)) + 'deg) saturate(4)', 'invert(1) hue-rotate(180deg)', 'none']);
          b.style.cssText = 'display:block;top:' + rand(-2, 98).toFixed(1) + '%;height:' + rand(0.6, 9).toFixed(1) + '%;' +
            'left:' + rand(-20, 10).toFixed(1) + '%;width:' + rand(40, 120).toFixed(1) + '%;' +
            (fx === 'none' ? 'background:' + pick(NOISE) + ';opacity:' + rand(0.4, 0.9).toFixed(2) + ';'
              : '-webkit-backdrop-filter:' + fx + ';backdrop-filter:' + fx + ';');
        });
        // The page itself jumps sideways now and then.
        body.style.transform = Math.random() < g * 0.6 ? 'translateX(' + Math.round(rand(-14, 14) * g) + 'px)' : '';
      }

      function run(out, done) {
        if (anim) anim.pause();
        layer.style.display = 'block';
        document.documentElement.style.overflowX = 'hidden';
        anim = stepper(duration, 45, function (t) { paint(t, out); }, function () {
          bars.forEach(function (b) { b.style.display = 'none'; });
          lines.style.display = 'none';
          body.style.transform = '';
          cover.style.display = out ? 'block' : 'none';
          if (!out) { layer.style.display = 'none'; document.documentElement.style.overflowX = ''; }
          if (done) done();
        });
      }
      if (arriving) run(false);
      unhide();

      document.addEventListener('click', function (e) {
        var url = linkFor(e);
        if (!url) return;
        e.preventDefault();
        var t = ++trip, gone = false;
        var go = function () { if (gone || t !== trip) return; gone = true; location.href = url.href; };
        try { sessionStorage.setItem(KEY, '1'); } catch (err) {}
        setTimeout(go, duration + 400);
        try { run(true, go); } catch (err) { go(); }
      });
      window.addEventListener('pageshow', function (e) {
        if (!e.persisted) return;
        trip++;
        try { sessionStorage.removeItem(KEY); } catch (err) {}
        run(false);
      });
    }

    // ---------- Contained mode: real copies of both views torn into slices ----------

    function copy(node, w, h) {
      var c = node.cloneNode(true);
      c.removeAttribute('id');
      c.querySelectorAll('[id]').forEach(function (n) { n.removeAttribute('id'); });
      c.classList.add('gtr-copy');
      c.style.width = w + 'px';
      c.style.height = h + 'px';
      c.style.visibility = 'visible';
      c.style.zIndex = '';
      return c;
    }

    function containedMode(el, still) {
      el.classList.add('gtr-contained');
      var slides = Array.from(el.children);
      var duration = +el.dataset.duration || DEFAULTS.slideDuration;
      var power = Math.max(0, Math.min(1, el.dataset.intensity === undefined ? DEFAULTS.intensity : +el.dataset.intensity));
      var i = 0;
      slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
      if (still) return;
      if (!defs.parentNode) document.body.appendChild(defs);

      function glitch() {
        var w = el.clientWidth, h = el.clientHeight;
        if (!w || !h) return;
        var n = (i + 1) % slides.length, from = slides[i], to = slides[n];
        var fx = document.createElement('div');
        fx.className = 'gtr-fx';
        fx.setAttribute('aria-hidden', 'true');

        // Channel split: a red copy and a green-blue copy, screened back together.
        function rgb(node) {
          var box = document.createElement('div');
          box.className = 'gtr-rgb';
          var r = box.appendChild(copy(node, w, h)), c = box.appendChild(copy(node, w, h));
          r.style.filter = 'url(#gtr-red)';
          c.style.filter = 'url(#gtr-cyan)';
          fx.appendChild(box);
          return { box: box, r: r, c: c };
        }
        var split = [rgb(from), rgb(to)];
        var slices = [];
        for (var k = 0; k < 7; k++) {
          var sl = document.createElement('div');
          sl.className = 'gtr-slice';
          var pair = [sl.appendChild(copy(from, w, h)), sl.appendChild(copy(to, w, h))];
          fx.appendChild(sl);
          slices.push({ el: sl, pair: pair });
        }
        var blocks = [];
        for (k = 0; k < 6; k++) blocks.push(fx.appendChild(document.createElement('i')));
        var lines = fx.appendChild(document.createElement('div'));
        lines.className = 'gtr-lines';
        el.appendChild(fx);

        stepper(duration, 50, function (t) {
          var g = envelope(t) * power;
          // Mostly the old view before the middle, mostly the new one after,
          // flickering between the two around the cut.
          var pTo = Math.max(0, Math.min(1, (t - 0.38) / 0.24));
          var showTo = Math.random() < pTo;
          from.style.visibility = showTo ? 'hidden' : 'visible';
          to.style.visibility = showTo ? 'visible' : 'hidden';

          split.forEach(function (s, side) { s.box.style.display = 'none'; });
          if (g > 0.2 && Math.random() < 0.75) {
            var s = split[showTo ? 1 : 0], off = rand(4, 14) * g * (Math.random() < 0.5 ? -1 : 1);
            s.box.style.display = 'block';
            s.r.style.transform = 'translate(' + off.toFixed(1) + 'px,' + (rand(-2, 2) * g).toFixed(1) + 'px)';
            s.c.style.transform = 'translateX(' + (-off).toFixed(1) + 'px)';
          }

          slices.forEach(function (sl) {
            if (Math.random() > g * 0.9) { sl.el.style.display = 'none'; return; }
            var top = rand(0, 0.95) * h, sh = rand(0.02, 0.16) * h, useTo = Math.random() < pTo;
            sl.el.style.display = 'block';
            sl.el.style.top = top.toFixed(1) + 'px';
            sl.el.style.height = sh.toFixed(1) + 'px';
            sl.el.style.transform = 'translateX(' + (rand(-0.18, 0.18) * w * g).toFixed(1) + 'px)';
            sl.el.style.filter = Math.random() < 0.3 ? pick(['invert(1)', 'hue-rotate(' + Math.round(rand(60, 300)) + 'deg) saturate(3)', 'brightness(1.8) contrast(1.4)']) : '';
            sl.pair[0].style.display = useTo ? 'none' : 'block';
            sl.pair[1].style.display = useTo ? 'block' : 'none';
            sl.pair[0].style.top = sl.pair[1].style.top = (-top).toFixed(1) + 'px';
          });

          blocks.forEach(function (b) {
            if (Math.random() > g * 0.5) { b.style.display = 'none'; return; }
            b.style.cssText = 'display:block;left:' + rand(-5, 90).toFixed(1) + '%;top:' + rand(0, 96).toFixed(1) + '%;' +
              'width:' + rand(4, 40).toFixed(1) + '%;height:' + rand(1, 5).toFixed(1) + '%;background:' + pick(NOISE) + ';opacity:' + rand(0.5, 0.95).toFixed(2);
          });
          lines.style.display = g > 0.1 ? 'block' : 'none';
        }, function () {
          fx.remove();
          from.style.visibility = 'hidden';
          to.style.visibility = 'visible';
          i = n;
        });
      }

      var timer = anime.createTimer({
        duration: (+el.dataset.interval || DEFAULTS.interval) + duration,
        loop: true,
        autoplay: false,
        onLoop: function () { if (!el.closest('[hidden]')) glitch(); }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) timer.play(); else timer.pause();
      }).observe(el);
    }

    function init() {
      var still = reduced();
      document.querySelectorAll('[data-glitch-transition]').forEach(function (el) {
        if (el._gtr) return;
        el._gtr = true;
        if (el === document.body) { if (!still) pageMode(el); }
        else if (el.children.length > 1) containedMode(el, still);
      });
      unhide();
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<body data-glitch-transition>
  …
</body>