Stack to Grid
A messy pile of photos deals itself out into a tidy grid, top photo first, each one gliding to its own cell. Click and they sweep back into the pile; click again and they spread out.
Click the photos to stack or spread them
How to use it
- Paste the snippet just before
</head>. - Add
data-stack-to-gridto a grid of photos. Lay the grid out with your own CSS: that’s where the photos end up.
Options
Add any of these attributes to the same element.
data-loop | Spread and stack on a timer, until someone clicks. |
data-interval | Time between changes when looping. Default 2800 ms. |
data-duration | Time for each photo to move. Default 750 ms. |
data-stagger | Gap between photos. Default 60 ms. |
data-angle | The most a photo tilts in the pile. Default 10°. |
data-trigger="none" | Clicking does nothing; drive it from code instead. |
It starts as a pile and spreads out when it scrolls into view. The grid is a toggle button (Enter and Space work too). From code, animacious.stackToGrid(element, false) stacks it and true spreads it out.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Stack to Grid: the photos start as a messy pile in the middle, then deal
// themselves out into their places in a tidy grid. Click (or tap) to sweep
// them back into a pile, and again to spread them out.
// Usage:
// <div data-stack-to-grid class="my-grid">
// <img src="one.jpg" alt="…"> <img src="two.jpg" alt="…"> …
// </div>
// Lay the children out as a grid with your own CSS: that is where they
// end up. The script only moves them there and back.
// Optional: data-loop (spread and stack on a timer), data-interval (ms
// between changes when looping, default 2800), data-duration (ms per photo,
// default 750), data-stagger (ms between photos, default 60),
// data-angle (most a photo tilts in the pile, degrees, default 10),
// data-trigger="none" (clicks do nothing).
// Script use: animacious.stackToGrid(element, true|false) spreads or stacks.
var DEFAULTS = { interval: 2800, duration: 750, stagger: 60, angle: 10 };
var css =
'[data-stack-to-grid]{position:relative}' +
'[data-stack-to-grid]>*{position:relative}' +
'[data-stack-to-grid].stg-click{cursor:pointer}' +
'[data-stack-to-grid]:focus-visible{outline:3px solid #a855f7;outline-offset:6px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var items = Array.from(el.children);
var n = items.length;
if (!n) return;
var duration = +d.duration || DEFAULTS.duration;
var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var interval = +d.interval || DEFAULTS.interval;
var angle = d.angle !== undefined ? +d.angle : DEFAULTS.angle;
var inThumb = !!el.closest('[aria-hidden="true"]');
var spread = false, timer = null, user = false;
// Each photo's own mess in the pile, kept so the pile looks the same every time.
var mess = items.map(function () {
return { x: (Math.random() - 0.5) * 34, y: (Math.random() - 0.5) * 22, rotate: (Math.random() * 2 - 1) * angle };
});
items.forEach(function (it, i) { it.style.zIndex = i + 1; });
// The offset that moves an item from its grid cell to the middle of the pile.
function pile(i) {
var it = items[i];
var cx = el.clientWidth / 2, cy = el.clientHeight / 2;
return {
x: cx - (it.offsetLeft + it.offsetWidth / 2) + mess[i].x,
y: cy - (it.offsetTop + it.offsetHeight / 2) + mess[i].y,
rotate: mess[i].rotate
};
}
function show(toGrid, animate) {
spread = toGrid;
el.classList.toggle('stg-stacked', !toGrid);
if (!inThumb && el.classList.contains('stg-click')) el.setAttribute('aria-pressed', toGrid ? 'true' : 'false');
items.forEach(function (it, i) {
var to = toGrid ? { x: 0, y: 0, rotate: 0 } : pile(i);
if (!animate || reduced()) { anime.utils.remove(it); return anime.utils.set(it, to); }
// Dealt from the top of the pile out; gathered in reverse.
var k = toGrid ? n - 1 - i : i;
to.duration = duration;
to.delay = k * stagger;
to.ease = toGrid ? 'out(4)' : 'inOut(3)';
to.scale = [{ to: toGrid ? 1.06 : 0.97, duration: duration * 0.4, ease: 'out(2)' }, { to: 1, duration: duration * 0.6, ease: 'out(3)' }];
anime.animate(it, to);
});
}
function toggle() { show(!spread, true); }
el._stg = function (on) { user = true; clearInterval(timer); show(on === undefined ? !spread : !!on, true); };
if (!inThumb && d.trigger !== 'none') {
el.classList.add('stg-click');
el.setAttribute('role', 'button');
if (!el.hasAttribute('tabindex')) el.tabIndex = 0;
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Spread out or stack the photos');
el.addEventListener('click', function () { user = true; clearInterval(timer); toggle(); });
el.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
user = true;
clearInterval(timer);
toggle();
});
}
show(reduced(), false);
if (window.ResizeObserver) new ResizeObserver(function () { if (!spread) show(false, false); }).observe(el);
if (reduced()) return;
var started = false;
new IntersectionObserver(function (entries) {
clearInterval(timer);
timer = null;
if (!entries[0].isIntersecting) return;
if (!started) { started = true; setTimeout(function () { show(true, true); }, 300); }
if (d.loop !== undefined && !user) {
timer = setInterval(function () { if (!el.closest('[hidden]')) toggle(); }, interval);
}
}, { threshold: 0.2 }).observe(el);
}
function init() {
document.querySelectorAll('[data-stack-to-grid]').forEach(function (el) {
if (el._stg) return;
el._stg = true;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.stackToGrid = function (el, on) { if (el && typeof el._stg === 'function') el._stg(on); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-stack-to-grid style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px">
<img src="one.jpg" alt="…">
<img src="two.jpg" alt="…">
<img src="three.jpg" alt="…">
<img src="four.jpg" alt="…">
<img src="five.jpg" alt="…">
<img src="six.jpg" alt="…">
</div>