← Page transitions

Flash Transition

A white flash punctuates the cut: the view burns out to white like a camera flash, cuts while the screen is blank, and the next view comes back a little overexposed before it settles. As a page transition the page flashes out when a link is clicked and the next page fades in from the glare. 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-flash-transition to each page’s <body>.

Options

Add any of these attributes to the same element.

data-flash-transition="#hex"Flash colour. Default #ffffff.
data-durationTime for the whole flash. Default 700 ms on a page, 900 ms in a slideshow.
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 fades the flash off again. Put data-flash-transition on any other element with two or more children and it flashes from one child to the next, like the demo above.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Flash Transition: a white flash punctuates the cut. The view burns out to
    // white like a camera flash, cuts while the screen is blank, and the next
    // view comes back slightly overexposed before settling.
    // Usage: <body data-flash-transition>   (add the snippet to every page)
    // Optional: data-flash-transition="#hex" (flash colour, default #ffffff),
    // data-duration (ms for the whole flash, default 700 on a page and 900 for
    // a slideshow change).
    // Contained mode: put data-flash-transition on any other element with two
    // or more children. Every data-interval ms (default 2200) it flashes to the
    // next child.
    var DEFAULTS = { color: '#ffffff', duration: 700, slideDuration: 900, interval: 2200 };
    var KEY = 'animacious-flash';

    var css =
      'html.fsh-pre body{opacity:0}' +
      '.fsh-layer{position:fixed;inset:0;z-index:2147483000;pointer-events:none;opacity:0}' +
      '.fsh-contained{position:relative;overflow:hidden;display:grid}' +
      '.fsh-contained>*{grid-area:1/1}' +
      '.fsh-contained>.fsh-flash{position:absolute;inset:0;z-index:2;pointer-events:none;opacity:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    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('fsh-pre'); }
    if (arriving && !reduced()) {
      document.documentElement.classList.add('fsh-pre');
      setTimeout(unhide, 3000);
    }

    function tween(duration, ease, paint, done) {
      var s = { t: 0 };
      paint(0);
      return anime.animate(s, {
        t: 1, duration: duration, ease: ease,
        onUpdate: function () { paint(s.t); },
        onComplete: function () { paint(1); if (done) done(); }
      });
    }

    // A flash is a soft-edged burst: brightest in the middle, filling out to
    // the edges as it peaks.
    function light(color, k) {
      var edge = Math.round(30 + 70 * k);
      return 'radial-gradient(circle at 50% 50%,' + color + ' ' + edge + '%,' + color + 'cc 100%)';
    }

    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;
    }

    function hex(c) {
      // Only #rgb / #rrggbb get the soft edge; other colours flash flat.
      return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c) ? (c.length === 4 ? '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3] : c) : null;
    }

    function pageMode(body) {
      var duration = +body.dataset.duration || DEFAULTS.duration;
      var color = body.getAttribute('data-flash-transition') || DEFAULTS.color;
      var soft = hex(color);
      var layer = document.createElement('div');
      layer.className = 'fsh-layer';
      layer.setAttribute('aria-hidden', 'true');
      layer.style.background = color;
      document.body.appendChild(layer);
      var anim, trip = 0;

      // Leaving: a fast burn up to the flash colour. Arriving: the flash fades
      // off the new page, more slowly.
      function run(out, done) {
        if (anim) anim.pause();
        anim = tween(out ? duration * 0.4 : duration * 0.6, out ? 'in(2)' : 'out(2)', function (t) {
          var k = out ? t : 1 - t;
          layer.style.opacity = k.toFixed(3);
          if (soft) layer.style.background = light(soft, k);
        }, 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 * 0.4 + 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);
      });
    }

    function containedMode(el, still) {
      el.classList.add('fsh-contained');
      var slides = Array.from(el.children);
      var duration = +el.dataset.duration || DEFAULTS.slideDuration;
      var color = el.getAttribute('data-flash-transition') || DEFAULTS.color;
      var soft = hex(color);
      var flash = document.createElement('div');
      flash.className = 'fsh-flash';
      flash.setAttribute('aria-hidden', 'true');
      flash.style.background = color;
      var i = 0;
      slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
      if (still) return;
      el.appendChild(flash);

      var PEAK = 0.2;   // the cut happens here, at the flash's brightest
      var timer = anime.createTimer({
        duration: (+el.dataset.interval || DEFAULTS.interval) + duration,
        loop: true,
        autoplay: false,
        onLoop: function () {
          if (el.closest('[hidden]')) return;
          var n = (i + 1) % slides.length, from = slides[i], to = slides[n], cut = false;
          tween(duration, 'linear', function (t) {
            var k, s;
            if (t < PEAK) {
              s = t / PEAK;
              k = s * s * s;                                         // burns up fast
              from.style.filter = 'brightness(' + (1 + 1.6 * k).toFixed(3) + ') saturate(' + (1 - 0.5 * k).toFixed(3) + ')';
              from.style.transform = 'scale(' + (1 + 0.03 * k).toFixed(4) + ')';
            } else {
              if (!cut) {
                cut = true;
                from.style.visibility = 'hidden';
                from.style.filter = from.style.transform = '';
                to.style.visibility = 'visible';
              }
              s = (t - PEAK) / (1 - PEAK);
              k = Math.pow(1 - s, 2.4);                              // fades off slowly
              to.style.filter = k > 0.002 ? 'brightness(' + (1 + 1.2 * k).toFixed(3) + ') contrast(' + (1 - 0.25 * k).toFixed(3) + ')' : '';
              to.style.transform = 'scale(' + (1 + 0.04 * k).toFixed(4) + ')';
            }
            flash.style.opacity = k.toFixed(3);
            if (soft) flash.style.background = light(soft, k);
          }, function () {
            to.style.filter = to.style.transform = '';
            flash.style.opacity = 0;
            i = n;
          });
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) timer.play(); else timer.pause();
      }).observe(el);
    }

    function init() {
      var still = reduced();
      document.querySelectorAll('[data-flash-transition]').forEach(function (el) {
        if (el._fsh) return;
        el._fsh = 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-flash-transition>
  …
</body>