← Page transitions

Push Slide

The next view slides in from the side and pushes the current one off the other edge, the two moving together as if on one strip. Tap or swipe the demo to push it along yourself.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-push-slide 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 750 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.pushSlide(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 () {
    // Push Slide: a section transition. The next view slides in from the side
    // and pushes the current one off the other edge, the two moving as one.
    // Usage: <div data-push-slide><div>One</div><div>Two</div>…</div>
    // Each child is a view. Tap or click to push on to the next; swipe back the
    // other way to return to the previous one.
    // Optional: data-direction ("left", the default: views travel leftwards;
    // or "right", "up", "down"), data-duration (ms, default 750),
    // data-interval (ms between automatic pushes, default 2600; "0" turns
    // them off). Automatic pushes rest for a while after someone interacts.
    // Script use: animacious.pushSlide(el, 1) for next, (el, -1) for previous.
    var DEFAULTS = { direction: 'left', duration: 750, interval: 2600 };

    var css =
      '.psl-on{position:relative;overflow:hidden;display:grid}' +
      '.psl-on>*{grid-area:1/1}' +
      '.psl-tap{cursor:pointer;touch-action:pan-y;-webkit-user-select:none;user-select:none}' +
      '.psl-tap.psl-y{touch-action:pan-x}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Which axis the views travel on, and which way "next" goes.
    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._psl;
      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];
      var t = travel(el), dir = t.sign * (step > 0 ? 1 : -1);
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        from.style.visibility = 'hidden';
        to.style.visibility = 'visible';
        return;
      }
      s.busy = true;
      to.style.visibility = 'visible';
      var a = {}, b = {};
      // The new view starts a full width (or height) away on the far side.
      a[t.prop] = ['0%', dir * 100 + '%'];
      b[t.prop] = [-dir * 100 + '%', '0%'];
      a.duration = b.duration = s.duration;
      a.ease = b.ease = 'inOut(4)';
      anime.createTimeline({
        onComplete: function () {
          from.style.visibility = 'hidden';
          anime.utils.set(from, { translateX: 0, translateY: 0 });
          s.busy = false;
        }
      }).add(from, a, 0).add(to, b, 0);
    }

    function setup(el) {
      el.classList.add('psl-on');
      var s = el._psl = {
        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('psl-tap');
        if (travel(el).prop === 'translateY') el.classList.add('psl-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();
          // A swipe the same way as "next" goes forward, the other way back.
          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.pushSlide = go;

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

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