← Page transitions

Barn Door Wipe

A page transition: when a link is clicked, two panels slide in from the sides and meet in the middle with a soft bump, and on the next page they part again to show it. 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-barn-door-wipe to each page’s <body>.

Options

Add any of these attributes to the same element.

data-barn-door-wipe="#hex"Panel colour. Default #1e1b4b.
data-direction="vertical"Panels close from the top and bottom instead.
data-durationTime for each close or open. Default 650 ms.
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 opens the doors again. Put data-barn-door-wipe on any other element with two or more children and it becomes a slideshow.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Barn Door Wipe: a page transition. When a link is clicked two panels
    // slide in from the sides and meet in the middle with a soft bump; on the
    // next page they part again to reveal it.
    // Usage: <body data-barn-door-wipe>   (add the snippet to every page)
    // Optional: data-barn-door-wipe="#hex" (panel colour, default #1e1b4b),
    // data-direction="vertical" (panels come from the top and bottom),
    // data-duration (ms for each close or open, default 650).
    // Contained mode: put data-barn-door-wipe on any other element with two or
    // more children and the doors close and part between them every
    // data-interval ms (default 2200).
    var DEFAULTS = { color: '#1e1b4b', duration: 650, interval: 2200 };

    var css =
      '.bdw-layer{position:fixed;inset:0;z-index:2147483000;pointer-events:none;visibility:hidden;overflow:hidden}' +
      '.bdw-door{position:absolute}' +
      // A bright seam along each meeting edge and a shadow cast on the page.
      '.bdw-a{box-shadow:inset -2px 0 0 rgba(255,255,255,.18),8px 0 24px rgba(0,0,0,.25)}' +
      '.bdw-b{box-shadow:inset 2px 0 0 rgba(255,255,255,.18),-8px 0 24px rgba(0,0,0,.25)}' +
      '.bdw-v .bdw-a{box-shadow:inset 0 -2px 0 rgba(255,255,255,.18),0 8px 24px rgba(0,0,0,.25)}' +
      '.bdw-v .bdw-b{box-shadow:inset 0 2px 0 rgba(255,255,255,.18),0 -8px 24px rgba(0,0,0,.25)}' +
      '.bdw-contained{position:relative;overflow:hidden;display:grid}' +
      '.bdw-contained>*:not(.bdw-layer){grid-area:1/1}' +
      '.bdw-contained>.bdw-layer{position:absolute}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function layer(parent, color, vertical) {
      var l = document.createElement('div');
      l.className = 'bdw-layer' + (vertical ? ' bdw-v' : '');
      l.setAttribute('aria-hidden', 'true');
      ['bdw-a', 'bdw-b'].forEach(function (cls, n) {
        var d = document.createElement('div');
        d.className = 'bdw-door ' + cls;
        // Each door reaches a touch past the middle, so they overlap with no
        // seam, and runs on past its outer edge, so the bump as they meet never
        // opens a gap at the sides.
        d.style.cssText = vertical
          ? 'left:0;right:0;height:calc(65% + 1px);' + (n ? 'bottom:-15%;' : 'top:-15%;')
          : 'top:0;bottom:0;width:calc(65% + 1px);' + (n ? 'right:-15%;' : 'left:-15%;');
        // A faint sheen, lighter towards the meeting edge.
        d.style.background = 'linear-gradient(' + (vertical ? (n ? 'to top' : 'to bottom') : (n ? 'to left' : 'to right')) +
          ', rgba(0,0,0,.18), rgba(255,255,255,.06)), ' + color;
        l.appendChild(d);
      });
      parent.appendChild(l);
      l._bdwDoors = Array.from(l.children);
      l._bdwProp = vertical ? 'y' : 'x';
      return l;
    }

    // Close: doors slide in from off-screen. Open: they slide back out.
    function doors(l, close, duration, done) {
      if (l._bdw) l._bdw.pause();
      var d = l._bdwDoors, p = l._bdwProp, a = {}, b = {};
      a[p] = close ? ['-100%', '0%'] : ['0%', '-100%'];
      b[p] = close ? ['100%', '0%'] : ['0%', '100%'];
      a.duration = b.duration = duration;
      a.ease = b.ease = close ? 'outBack(1.15)' : 'inOut(3)';
      l.style.visibility = 'visible';
      var tl = anime.createTimeline({
        onComplete: function () { if (!close) l.style.visibility = 'hidden'; if (done) done(); }
      });
      tl.add(d[0], a, 0).add(d[1], b, 0);
      l._bdw = tl;
    }

    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 options(el) {
      return [el.getAttribute('data-barn-door-wipe') || DEFAULTS.color, el.dataset.direction === 'vertical'];
    }

    function pageMode(body) {
      var duration = +body.dataset.duration || DEFAULTS.duration;
      var o = options(body), l = layer(body, o[0], o[1]);
      var trip = 0;
      doors(l, false, duration);

      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 {
          doors(l, true, duration, go);
          setTimeout(go, duration + 400);
        } catch (err) { go(); }
      });
      window.addEventListener('pageshow', function (e) {
        if (!e.persisted) return;
        trip++;
        doors(l, false, duration);
      });
    }

    function containedMode(el, reduced) {
      el.classList.add('bdw-contained');
      var slides = Array.from(el.children);
      var duration = +el.dataset.duration || DEFAULTS.duration;
      var o = options(el), l = layer(el, o[0], o[1]);
      var i = 0;
      slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
      if (reduced) return;
      var timer = anime.createTimer({
        duration: (+el.dataset.interval || DEFAULTS.interval) + duration * 2 + 200,
        loop: true,
        autoplay: false,
        onLoop: function () {
          if (el.closest('[hidden]')) return;
          doors(l, true, duration, function () {
            slides[i].style.visibility = 'hidden';
            i = (i + 1) % slides.length;
            slides[i].style.visibility = 'visible';
            setTimeout(function () { doors(l, false, duration); }, 200);
          });
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) timer.play(); else timer.pause();
      }).observe(el);
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      document.querySelectorAll('[data-barn-door-wipe]').forEach(function (el) {
        if (el._bdw) return;
        el._bdw = true;
        if (el === document.body) { if (!reduced) pageMode(el); }
        else if (el.children.length > 1) containedMode(el, reduced);
      });
    }

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