← Page transitions

Slide-over

The next view slides in over the current one, which stays put and dims as it’s covered, like a card laid on a stack. Swipe back and the top view slides away to uncover it again. Tap or swipe the demo to try it.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-slide-over to a container whose children are the views, one after another.

Options

Add any of these attributes to the same element.

data-directionWhich way views travel: left, right, up or down. Default left.
data-durationTime for each slide. Default 700 ms.
data-intervalTime between automatic slides. Default 2600 ms; 0 turns them off.

Tap or click to move on to the next view, or swipe back the other way for the previous one; automatic slides rest for a few seconds after that. Call animacious.slideOver(el, 1) or (el, -1) to drive it from your own buttons. With reduced motion the views simply swap.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Slide-over: a section transition. The next view slides in over the
    // current one, which stays put and dims into the background as it is
    // covered, like a card laid on top of a stack. Going back slides the top
    // view away to uncover the one beneath.
    // Usage: <div data-slide-over><div>One</div><div>Two</div>…</div>
    // Each child is a view. Tap or click to slide the next one over; swipe the
    // other way to slide back.
    // Optional: data-direction ("left", the default: new views arrive from the
    // right and travel left; or "right", "up", "down"), data-duration (ms,
    // default 700), data-interval (ms between automatic slides, default 2600;
    // "0" turns them off). Automatic slides rest for a while after someone
    // interacts.
    // Script use: animacious.slideOver(el, 1) for next, (el, -1) for previous.
    var DEFAULTS = { direction: 'left', duration: 700, interval: 2600 };

    var css =
      '.sov-on{position:relative;overflow:hidden;display:grid}' +
      '.sov-on>*{grid-area:1/1}' +
      '.sov-top{z-index:2;box-shadow:0 0 40px rgba(0,0,0,.45)}' +
      '.sov-tap{cursor:pointer;touch-action:pan-y;-webkit-user-select:none;user-select:none}' +
      '.sov-tap.sov-y{touch-action:pan-x}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function travel(el) {
      var d = el.dataset.direction || DEFAULTS.direction;
      return { prop: d === 'up' || d === 'down' ? 'translateY' : 'translateX', sign: d === 'right' || d === 'down' ? 1 : -1 };
    }

    function go(el, step) {
      var s = el._sov;
      if (!s || s.busy || s.slides.length < 2) return;
      var from = s.slides[s.i];
      s.i = (s.i + step + s.slides.length) % s.slides.length;
      var to = s.slides[s.i];
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        from.style.visibility = 'hidden';
        to.style.visibility = 'visible';
        return;
      }
      var t = travel(el), forward = step > 0;
      // Forward: the new view moves over the still one. Back: the current view
      // moves away off the top of the old one.
      var mover = forward ? to : from, still = forward ? from : to;
      var off = -t.sign * 100 + '%';
      var move = {}, dim = {};
      move[t.prop] = forward ? [off, '0%'] : ['0%', off];
      move.duration = dim.duration = s.duration;
      move.ease = 'inOut(4)';
      dim.filter = forward ? ['brightness(1)', 'brightness(0.55)'] : ['brightness(0.55)', 'brightness(1)'];
      dim.ease = 'inOut(2)';
      s.busy = true;
      to.style.visibility = 'visible';
      mover.classList.add('sov-top');
      anime.createTimeline({
        onComplete: function () {
          from.style.visibility = 'hidden';
          mover.classList.remove('sov-top');
          anime.utils.set([from, to], { translateX: 0, translateY: 0, filter: 'none' });
          s.busy = false;
        }
      }).add(mover, move, 0).add(still, dim, 0);
    }

    function setup(el) {
      el.classList.add('sov-on');
      var s = el._sov = {
        slides: Array.from(el.children),
        i: 0,
        busy: false,
        duration: +el.dataset.duration || DEFAULTS.duration,
        touched: 0
      };
      s.slides.forEach(function (c, n) { c.style.visibility = n ? 'hidden' : 'visible'; });

      // Taps and swipes: not inside a link (a home-page card), so the card's
      // own click still works there.
      if (!el.closest('a')) {
        el.classList.add('sov-tap');
        if (travel(el).prop === 'translateY') el.classList.add('sov-y');
        var x0 = null, y0 = 0;
        el.addEventListener('pointerdown', function (e) { x0 = e.clientX; y0 = e.clientY; });
        el.addEventListener('pointerup', function (e) {
          if (x0 === null) return;
          var t = travel(el), d = t.prop === 'translateX' ? e.clientX - x0 : e.clientY - y0;
          var across = t.prop === 'translateX' ? e.clientY - y0 : e.clientX - x0;
          x0 = null;
          if (Math.abs(across) > 40 && Math.abs(across) > Math.abs(d)) return;
          s.touched = Date.now();
          if (Math.abs(d) > 40) go(el, (d > 0 ? 1 : -1) === t.sign ? 1 : -1);
          else if (Math.abs(d) < 10) go(el, 1);
        });
        el.addEventListener('pointercancel', function () { x0 = null; });
      }

      var interval = el.dataset.interval === undefined ? DEFAULTS.interval : +el.dataset.interval;
      if (!interval || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      var timer = anime.createTimer({
        duration: interval + s.duration,
        loop: true,
        autoplay: false,
        onLoop: function () {
          if (el.closest('[hidden]') || Date.now() - s.touched < interval * 2) return;
          go(el, 1);
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) timer.play(); else timer.pause();
      }).observe(el);
    }

    window.animacious = window.animacious || {};
    window.animacious.slideOver = go;

    function init() {
      document.querySelectorAll('[data-slide-over]').forEach(function (el) {
        if (el._sov || el.children.length < 2) return;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-slide-over>
  <section>First view…</section>
  <section>Second view…</section>
  <section>Third view…</section>
</div>