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
- Paste the snippet just before
</head>. - Add
data-card-stack-shuffleto a container whose children are the cards.
Options
Add any of these attributes to the same element.
data-interval | Time between shuffles. Default 2600 ms. |
data-trigger="click" | Shuffle when clicked instead of on a timer. |
data-offset | How far each card behind peeks out, in px. Default 14. |
<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><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>