← Page transitions

Curtain Wipe

A page transition: a coloured curtain sweeps up to cover the page when a link is clicked, and lifts away when the next page opens. This page uses it, so try any link.

Page one
Page two

How to use it

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

Options

Add any of these attributes to the same element.

data-curtain-wipe="#hex"Curtain colour. Default #6a0dad.
data-durationTime for each sweep. Default 650 ms.
data-intervalContained mode only: time each slide stays. Default 2400 ms.

Links to other sites, new tabs, downloads and #anchors are left alone. Put data-curtain-wipe on any other element with two or more children and it becomes a slideshow that wipes between them.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Curtain Wipe: a page transition. A coloured curtain sweeps up to cover the
    // page when a link is clicked, and lifts away again when the next page opens.
    // Usage: <body data-curtain-wipe>   (add the snippet to every page)
    // Optional: data-curtain-wipe="#6a0dad" (curtain colour), data-duration (ms).
    // Contained mode: put data-curtain-wipe on any other element with two or
    // more children and it wipes between them every data-interval ms (a slideshow).
    var DEFAULTS = { color: '#6a0dad', duration: 650, interval: 2400 };

    var css =
      '.cw-panel{position:fixed;inset:0;z-index:2147483000;pointer-events:none;transform:scaleY(0)}' +
      '.cw-contained{position:relative;overflow:hidden;display:grid}' +
      '.cw-contained>*:not(.cw-panel){grid-area:1/1}' +
      '.cw-contained>.cw-panel{position:absolute}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function panel(parent, color) {
      var p = document.createElement('div');
      p.className = 'cw-panel';
      p.setAttribute('aria-hidden', 'true');
      p.style.background = color;
      parent.appendChild(p);
      return p;
    }

    // Cover: grows up from the bottom. Reveal: shrinks away upwards.
    function cover(p, duration, done) {
      anime.utils.set(p, { transformOrigin: '50% 100%' });
      anime.animate(p, { scaleY: [0, 1], duration: duration, ease: 'inOut(3)', onComplete: done });
    }
    function reveal(p, duration, done) {
      anime.utils.set(p, { transformOrigin: '50% 0%' });
      anime.animate(p, { scaleY: [1, 0], duration: duration, ease: 'inOut(3)', onComplete: done });
    }

    function pageMode(body) {
      var color = body.getAttribute('data-curtain-wipe') || DEFAULTS.color;
      var duration = +body.dataset.duration || DEFAULTS.duration;
      var p = panel(body, color);
      anime.utils.set(p, { scaleY: 1 });
      reveal(p, duration);

      document.addEventListener('click', function (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;
        if (a.target && a.target !== '_self') return;
        if (a.hasAttribute('download')) return;
        var url = new URL(a.href, location.href);
        if (url.origin !== location.origin) return;
        // Same page, only the #hash changes: let the browser scroll.
        if (url.pathname === location.pathname && url.search === location.search && url.hash) return;
        e.preventDefault();
        cover(p, duration, function () { location.href = url.href; });
      });
      // Coming back with the Back button can restore the covered page: lift it.
      window.addEventListener('pageshow', function (e) {
        if (e.persisted) anime.utils.set(p, { scaleY: 0 });
      });
    }

    function containedMode(el) {
      el.classList.add('cw-contained');
      var slides = Array.from(el.children);
      var color = el.getAttribute('data-curtain-wipe') || DEFAULTS.color;
      var duration = +el.dataset.duration || DEFAULTS.duration;
      var p = panel(el, color);
      var i = 0;
      slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
      setInterval(function () {
        cover(p, duration, function () {
          slides[i].style.visibility = 'hidden';
          i = (i + 1) % slides.length;
          slides[i].style.visibility = 'visible';
          reveal(p, duration);
        });
      }, (+el.dataset.interval || DEFAULTS.interval) + duration * 2);
    }

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

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