← Hover & cursor

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.

Lakes12 photos
Aurora8 photos
Dunes15 photos

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-focus-cards to 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-blurHow much the other cards blur, in px. Default 4.
data-dimOpacity of the other cards. Default 0.45.
data-scaleHow much the focused card grows. Default 1.03.
data-autoplayFocus 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.

1 · Snippet
<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>
2 · Your HTML
<div class="cards" data-focus-cards>
  <article class="card">…</article>
  <article class="card">…</article>
  <article class="card">…</article>
</div>