Focus Cards
Hover one card in a group and the others soften: they blur, dim and step back a touch, while the one you’re on comes forward. Move off the group and they all come back into focus.
How to use it
- Paste the snippet just before
</head>. - Add
data-focus-cardsto the container of a group of cards. Every direct child counts as a card.
Options
Add any of these attributes to the same element.
data-blur | How much the other cards blur, in px. Default 4. |
data-dim | Opacity of the other cards. Default 0.45. |
data-scale | How much the focused card grows. Default 1.03. |
data-autoplay | Focus each card in turn while the pointer isn’t over the group. Handy for demos and touch screens. |
Keyboard focus works the same way. On touch screens, tap a card to focus it and tap again to clear.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Focus Cards: hover one card in a group and the others blur, dim and step
// back a little, so the one you're on stands out.
// Usage: <div data-focus-cards><div class="card">…</div><div class="card">…</div></div>
// Optional: data-blur (px the other cards blur, default 4), data-dim
// (their opacity, default 0.45), data-scale (how much the focused card
// grows, default 1.03), data-autoplay (focus each card in turn while the
// pointer isn't over the group; handy for demos and touch screens).
var DEFAULTS = { blur: 4, dim: 0.45, scale: 1.03 };
var css =
'.fcd>*{transition:filter .45s ease,opacity .45s ease,transform .5s cubic-bezier(.2,.8,.2,1)}' +
'.fcd-on>*{filter:blur(var(--fcd-blur));opacity:var(--fcd-dim);transform:scale(.97)}' +
'.fcd-on>.fcd-focus{filter:none;opacity:1;transform:scale(var(--fcd-scale))}' +
'@media (prefers-reduced-motion:reduce){.fcd>*{transition:none}}';
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;
el.classList.add('fcd');
el.style.setProperty('--fcd-blur', (d.blur !== undefined ? +d.blur : DEFAULTS.blur) + 'px');
el.style.setProperty('--fcd-dim', d.dim !== undefined ? +d.dim : DEFAULTS.dim);
el.style.setProperty('--fcd-scale', d.scale !== undefined ? +d.scale : DEFAULTS.scale);
var current = null;
function cardOf(node) {
while (node && node.parentElement !== el) node = node.parentElement;
return node;
}
function focus(card) {
if (current === card) return;
if (current) current.classList.remove('fcd-focus');
current = card;
if (card) card.classList.add('fcd-focus');
el.classList.toggle('fcd-on', !!card);
}
el.addEventListener('pointerover', function (e) {
if (e.pointerType === 'touch') return;
el._fcdReal = true;
var card = cardOf(e.target);
if (card) focus(card);
});
el.addEventListener('pointerleave', function (e) {
if (e.pointerType === 'touch') return;
el._fcdReal = false;
focus(null);
});
// Touch: tap a card to focus it, tap it again (or between cards) to clear.
el.addEventListener('pointerdown', function (e) {
if (e.pointerType !== 'touch') return;
el._fcdHold = Date.now() + 5000;
var card = cardOf(e.target);
focus(card && card !== current ? card : null);
});
el.addEventListener('focusin', function (e) { focus(cardOf(e.target)); });
el.addEventListener('focusout', function (e) { if (!el.contains(e.relatedTarget)) focus(null); });
return { focus: focus, current: function () { return current; } };
}
// Focus each card in turn, then let them all rest for a moment.
function autoplay(el, api) {
var visible = false, timer = null, i = 0, mine = false;
function tick() {
if (!visible) return;
var busy = el._fcdReal || (el._fcdHold && Date.now() < el._fcdHold) || el.contains(document.activeElement);
if (busy) { mine = false; timer = setTimeout(tick, 800); return; }
if (!mine && api.current()) { timer = setTimeout(tick, 800); return; }
var cards = el.children;
if (i < cards.length) {
api.focus(cards[i++]);
mine = true;
timer = setTimeout(tick, 1500);
} else {
api.focus(null);
mine = false;
i = 0;
timer = setTimeout(tick, 1100);
}
}
new IntersectionObserver(function (entries) {
var was = visible;
visible = entries[0].isIntersecting;
if (visible && !was) { clearTimeout(timer); timer = setTimeout(tick, 400); }
}).observe(el);
}
function init() {
document.querySelectorAll('[data-focus-cards]').forEach(function (el) {
if (el._fcd) return;
el._fcd = true;
var api = setup(el);
var auto = el.dataset.autoplay;
if (auto !== undefined && auto !== 'false' && !reduced()) autoplay(el, api);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="cards" data-focus-cards>
<article class="card">…</article>
<article class="card">…</article>
<article class="card">…</article>
</div>