← Cards & images

Holographic Card

A collectible card with a rainbow foil finish. Move the pointer over it and the card tilts towards you while a band of foil colours slides across and a glare follows, like a shiny trading card turned under a lamp.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-holographic-card to your card and give it a size. Rounded corners are kept.

Options

Add any of these attributes to the same element.

data-tiltThe most the card tilts. Default 14°.
data-foilStrength of the rainbow foil, from 0 to 1. Default 0.45.
data-autoplayTilt slowly by itself until the pointer takes over, and again a moment after it leaves.

On touch screens, drag a finger across the card to tilt it (the card doesn’t scroll the page while you do). The foil and glare are layers the script adds on top, so your card’s own content stays as it is. With reduced motion the card stays still, catching the light at an angle.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Holographic Card: a collectible card with a rainbow foil finish. As the
    // pointer moves over it the card tilts towards it, a band of foil colours
    // slides across and a glare follows, like tilting a shiny trading card
    // under a lamp. On touch screens, drag a finger over it.
    // Usage: <div data-holographic-card>…your card…</div>
    // Give the card a size; rounded corners are kept.
    // Optional: data-tilt (most it tilts, degrees, default 14), data-foil
    // (strength of the foil, 0 to 1, default 0.45), data-autoplay (tilt
    // slowly by itself until the pointer takes over, and again after it leaves).
    var DEFAULTS = { tilt: 14, foil: 0.45 };

    var css =
      '[data-holographic-card]{position:relative;overflow:hidden;isolation:isolate;touch-action:none;will-change:transform;--hlc-x:50%;--hlc-y:50%;--hlc-o:0}' +
      '.hlc-foil,.hlc-glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:10}' +
      '.hlc-foil{background-image:repeating-linear-gradient(-45deg,rgba(255,255,255,.14) 0 1px,transparent 1px 5px),repeating-linear-gradient(115deg,#ff2a6d 0%,#ffcf3f 6%,#3dffa8 12%,#28c7ff 18%,#a259ff 24%,#ff2a6d 30%);background-size:100% 100%,400% 400%;background-position:0 0,var(--hlc-x) var(--hlc-y);mix-blend-mode:color-dodge;opacity:calc(var(--hlc-o) * var(--hlc-f));filter:saturate(1.3) contrast(1.1)}' +
      '.hlc-glare{background:radial-gradient(farthest-corner circle at var(--hlc-x) var(--hlc-y),rgba(255,255,255,.8) 0%,rgba(255,255,255,.25) 22%,rgba(0,0,0,.28) 90%);mix-blend-mode:overlay;opacity:var(--hlc-o)}';
    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 tilt = d.tilt !== undefined ? +d.tilt : DEFAULTS.tilt;
      var foil = d.foil !== undefined ? +d.foil : DEFAULTS.foil;
      var auto = d.autoplay !== undefined && d.autoplay !== 'false';
      el.style.setProperty('--hlc-f', foil);
      ['hlc-foil', 'hlc-glare'].forEach(function (c) {
        var layer = document.createElement('span');
        layer.className = c;
        layer.setAttribute('aria-hidden', 'true');
        el.appendChild(layer);
      });

      // Still frame: catch the light at an angle, no tilt.
      if (reduced()) {
        el.style.setProperty('--hlc-x', '30%');
        el.style.setProperty('--hlc-y', '25%');
        el.style.setProperty('--hlc-o', '0.7');
        return;
      }

      // "now" eases towards "goal" every frame. x and y are 0..1 across the card.
      var now = { x: 0.5, y: 0.5, o: 0 }, goal = { x: 0.5, y: 0.5, o: 0 };
      var raf = 0, visible = false, pointer = false, idleAt = 0, t0 = performance.now();

      function frame(t) {
        raf = 0;
        if (!pointer && auto && t > idleAt) {
          var s = (t - t0) / 1000;
          goal.x = 0.5 + 0.4 * Math.sin(s * 0.9);
          goal.y = 0.5 + 0.32 * Math.sin(s * 1.4 + 1);
          goal.o = 1;
        }
        var k = pointer ? 0.18 : 0.08, moving = false;
        ['x', 'y', 'o'].forEach(function (p) {
          now[p] += (goal[p] - now[p]) * k;
          if (Math.abs(goal[p] - now[p]) > 0.001) moving = true;
        });
        var ry = (now.x - 0.5) * 2 * tilt, rx = -(now.y - 0.5) * 2 * tilt;
        el.style.transform = 'perspective(900px) rotateX(' + rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg) scale(' + (1 + now.o * 0.03).toFixed(4) + ')';
        el.style.setProperty('--hlc-x', (now.x * 100).toFixed(1) + '%');
        el.style.setProperty('--hlc-y', (now.y * 100).toFixed(1) + '%');
        el.style.setProperty('--hlc-o', now.o.toFixed(3));
        if (visible && (moving || pointer || (auto && !pointer))) raf = requestAnimationFrame(frame);
      }
      function kick() { if (!raf && visible) raf = requestAnimationFrame(frame); }

      function aim(e) {
        var r = el.getBoundingClientRect();
        goal.x = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
        goal.y = Math.max(0, Math.min(1, (e.clientY - r.top) / r.height));
        goal.o = 1;
        pointer = true;
        kick();
      }
      function leave() {
        pointer = false;
        goal.x = goal.y = 0.5;
        goal.o = 0;
        idleAt = performance.now() + 1500;   // autoplay picks up again after a pause
        kick();
      }

      el.addEventListener('pointermove', aim);
      el.addEventListener('pointerdown', function (e) {
        aim(e);
        if (e.pointerType !== 'mouse') el.setPointerCapture(e.pointerId);
      });
      el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') leave(); });
      el.addEventListener('pointercancel', leave);
      el.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse' || e.pointerType === 'pen') leave(); });

      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        if (visible) kick();
        else if (raf) { cancelAnimationFrame(raf); raf = 0; }
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-holographic-card]').forEach(function (el) {
        if (el._hlc) return;
        el._hlc = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-holographic-card data-autoplay class="my-card">
  <img src="dragon.jpg" alt="Ember Dragon">
  <h3>Ember Dragon</h3>
</div>