← Cards & images

Card Stack Shuffle

Cards sit in a neat stack. The top one swipes away and slides in at the back, revealing the next. Good for testimonials, features or photos.

Fast
Simple
Yours

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-card-stack-shuffle to a container whose children are the cards.

Options

Add any of these attributes to the same element.

data-intervalTime between shuffles. Default 2600 ms.
data-trigger="click"Shuffle when clicked instead of on a timer.
data-offsetHow far each card behind peeks out, in px. Default 14.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Card Stack Shuffle: cards sit in a neat stack; the top one swipes away and
    // slides in at the back, revealing the next. Good for testimonials or features.
    // Usage: <div data-card-stack-shuffle> <div>Card 1</div> <div>Card 2</div> … </div>
    // Optional: data-interval (ms between shuffles, default 2600), data-trigger=
    // "click" (shuffle on click instead of on a timer), data-offset (px each card
    // behind peeks out, default 14).
    var DEFAULTS = { interval: 2600, offset: 14 };

    var css =
      '[data-card-stack-shuffle]{display:grid}' +
      '[data-card-stack-shuffle]>*{grid-area:1/1;will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(el) {
      var d = el.dataset;
      var cards = Array.from(el.children);
      var offset = +d.offset || DEFAULTS.offset;
      var busy = false;

      function place(card, depth, animate) {
        card.style.zIndex = cards.length - depth;
        var props = {
          y: -depth * offset,
          scale: 1 - depth * 0.06,
          opacity: depth > 2 ? 0 : 1 - depth * 0.15
        };
        if (!animate) return anime.utils.set(card, props);
        props.duration = 700;
        props.ease = 'outBack(1.6)';
        anime.animate(card, props);
      }
      cards.forEach(function (c, i) { place(c, i, false); });

      function shuffle() {
        if (busy || cards.length < 2) return;
        busy = true;
        var top = cards[0];
        anime.animate(top, {
          y: 40, x: 60, rotate: 12, opacity: 0,
          duration: 450,
          ease: 'in(2)',
          onComplete: function () {
            cards.push(cards.shift());
            // Drop it in at the very back, then let everything settle forward.
            place(top, cards.length - 1, false);
            anime.utils.set(top, { x: 0, rotate: 0 });
            cards.forEach(function (c, i) { place(c, i, true); });
            setTimeout(function () { busy = false; }, 350);
          }
        });
      }
      return shuffle;
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      document.querySelectorAll('[data-card-stack-shuffle]').forEach(function (el) {
        if (el._cs) return;
        var shuffle = (el._cs = setup(el));
        if (reduced) return;
        if (el.dataset.trigger === 'click') {
          el.addEventListener('click', shuffle);
          el.style.cursor = 'pointer';
          return;
        }
        var timer = null;
        new IntersectionObserver(function (entries) {
          entries.forEach(function (e) {
            clearInterval(timer);
            if (e.isIntersecting) timer = setInterval(shuffle, +el.dataset.interval || DEFAULTS.interval);
          });
        }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-card-stack-shuffle>
  <div class="card">“Loved it.”</div>
  <div class="card">“So easy.”</div>
  <div class="card">“Ten out of ten.”</div>
</div>