Open demo

CSS code

HTML
<div class="duo">
  <div><span class="lbl">Event pass with a moving glare</span>
  <div class="tl" data-tilt="16">
    <div class="tl-card tl-pass">
      <div class="tl-glare" aria-hidden="true"></div>
      <div class="tl-pass-top tl-z" style="--z:24px"><span class="tl-brand">FIELDNOTES 26</span><span class="tl-role">Speaker</span></div>
      <p class="tl-holder tl-z" style="--z:40px">Ama Owusu</p>
      <p class="tl-meta tl-z" style="--z:30px">Lisbon · 14 to 16 May 2026 · Hall C</p>
      <div class="tl-pass-foot tl-z" style="--z:24px"><span class="tl-no">Pass FN-0412</span>
        <a class="tl-wallet" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="6" width="18" height="13" rx="2.5"/><path d="M3 10h18M16 14.5h2"/></svg>Add to wallet</a></div>
    </div>
  </div></div>

  <div><span class="lbl">Layered product card</span>
  <div class="tl" data-tilt="14">
    <article class="tl-card tl-prod" aria-labelledby="tl-lamp">
      <div class="tl-stage">
        <svg class="tl-z" viewBox="0 0 120 140" aria-hidden="true" focusable="false"><ellipse cx="98" cy="74" rx="16" ry="9" transform="rotate(-42 98 74)" fill="#fff7ed" opacity=".9"/><rect x="26" y="122" width="58" height="10" rx="5" fill="#7c2d12"/><path d="M55 122L37 80l38-34" stroke="#9a3412" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="37" cy="80" r="5.5" fill="#7c2d12"/><circle cx="75" cy="46" r="5" fill="#7c2d12"/><path d="M70 41l12-9 30 27-18 19z" fill="#ea580c"/><path d="M82 32l30 27" stroke="#fdba74" stroke-width="2.5" stroke-linecap="round"/></svg>
        <span class="tl-price tl-z">€89</span>
      </div>
      <h3 class="tl-z" id="tl-lamp">Halo desk lamp</h3>
      <p class="tl-z">Warm dimmable LED with three colour temperatures and a weighted steel base.</p>
      <button class="tl-buy tl-z" type="button">Add to cart<span class="sr-only">: Halo desk lamp</span></button>
    </article>
  </div></div>
</div>

<div><span class="lbl">Feature cards with a gentle tilt</span>
<ul class="tl-trio">
  <li class="tl tl-sm" data-tilt="8"><div class="tl-card tl-feat">
    <span class="tl-ico tl-z"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18a4.5 4.5 0 0 1-.9-8.9A6 6 0 0 1 17.7 8 4.5 4.5 0 0 1 17 18"/><path d="M12 12v8M9 17l3 3 3-3"/></svg></span>
    <h3>Offline sync</h3><p>Edit on the train. Changes merge the moment you are back online.</p>
    <a class="tl-more" href="#">Learn more<span class="sr-only"> about offline sync</span></a></div></li>
  <li class="tl tl-sm" data-tilt="8"><div class="tl-card tl-feat">
    <span class="tl-ico tl-z"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3M12 14.5v2"/></svg></span>
    <h3>Private by default</h3><p>Notes are encrypted on your device before they reach our servers.</p>
    <a class="tl-more" href="#">Learn more<span class="sr-only"> about encryption</span></a></div></li>
  <li class="tl tl-sm" data-tilt="8"><div class="tl-card tl-feat">
    <span class="tl-ico tl-z"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 12a8 8 0 1 0 2.3-5.7L4 8.5"/><path d="M4 4v4.5h4.5M12 8v4.5l3 2"/></svg></span>
    <h3>Version history</h3><p>Every save is kept for 90 days, so any page can go back to Tuesday.</p>
    <a class="tl-more" href="#">Learn more<span class="sr-only"> about version history</span></a></div></li>
</ul></div>
CSS
/* 3D tilt cards. .tl is the scene (perspective); .tl-card turns by --rx / --ry, which a few lines of script set from the
   pointer position (mouse and pen only, never touch or reduced motion). Keyboard focus inside a card gives it a fixed tilt
   instead. --gx / --gy move the glare. Children with .tl-z sit in front of the card plane for a layered look. */
.tl{--tl-ink:#0f172a;--tl-muted:#475569;--tl-accent:#4338ca;--tl-focus:#4338ca;--tl-tilt-x:6deg;--tl-tilt-y:-9deg;
  perspective:900px;color:var(--tl-ink)}
.tl :where(h3,p,ul){margin:0;padding:0}
.tl-card{position:relative;border-radius:22px;transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));transform-style:preserve-3d;
  transition:transform .6s cubic-bezier(.2,.8,.2,1),box-shadow .6s;will-change:transform}
.tl.is-tilting .tl-card{transition:transform .08s linear,box-shadow .3s}
.tl:focus-within{--rx:var(--tl-tilt-x);--ry:var(--tl-tilt-y)}
.tl-glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .4s;
  background:radial-gradient(circle at var(--gx,28%) var(--gy,18%),rgba(255,255,255,.5),rgba(255,255,255,0) 52%);mix-blend-mode:soft-light}
.tl.is-tilting .tl-glare,.tl:focus-within .tl-glare{opacity:1}
.tl-z{transform:translateZ(var(--z,36px))}
.tl a:focus-visible,.tl button:focus-visible{outline:3px solid var(--tl-focus);outline-offset:3px}
.tl .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* event pass */
.tl-pass{--tl-focus:#fde68a;max-width:380px;aspect-ratio:1.586;padding:22px 24px;display:flex;flex-direction:column;color:#fff;
  background:repeating-linear-gradient(115deg,rgba(255,255,255,0) 0 14px,rgba(255,255,255,.05) 14px 28px),linear-gradient(135deg,#1e1b4b,#4338ca 55%,#0e7490);
  box-shadow:0 30px 50px -28px rgba(30,27,75,.75)}
.tl-pass .tl-glare{mix-blend-mode:overlay;background:radial-gradient(circle at var(--gx,28%) var(--gy,18%),rgba(255,255,255,.7),rgba(255,255,255,0) 50%),
  linear-gradient(115deg,rgba(244,114,182,0) 30%,rgba(244,114,182,.35) 45%,rgba(56,189,248,.35) 55%,rgba(56,189,248,0) 70%)}
.tl-pass-top{display:flex;justify-content:space-between;align-items:center}
.tl-brand{font-weight:800;letter-spacing:.08em;font-size:.9rem}
.tl-role{padding:4px 10px;border-radius:999px;background:#fde68a;color:#422006;font-size:.7rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase}
.tl-holder{margin-top:auto;font-size:1.5rem;font-weight:800;letter-spacing:-.01em}
.tl-meta{margin-top:4px;font-size:.86rem;color:#e0e7ff}
.tl-pass-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;margin-top:16px}
.tl-no{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;color:#c7d2fe}
.tl-wallet{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:999px;background:#fff;color:#1e1b4b;font-size:.82rem;font-weight:700;text-decoration:none}
.tl-wallet svg{width:15px;height:15px}
.tl-wallet:hover{background:#e0e7ff}

/* layered product card */
.tl-prod{max-width:340px;padding:18px;background:#fff;border:1px solid #e2e8f0;box-shadow:0 30px 50px -32px rgba(15,23,42,.5)}
.tl-stage{position:relative;height:190px;border-radius:16px;background:linear-gradient(160deg,#fef3c7,#fdba74);transform-style:preserve-3d;display:grid;place-items:center}
.tl-stage svg{width:132px;height:150px;--z:54px;filter:drop-shadow(0 18px 14px rgba(124,45,18,.28))}
.tl-price{position:absolute;top:14px;right:14px;--z:70px;padding:6px 12px;border-radius:999px;background:#0f172a;color:#fff;font-weight:800;font-size:.9rem}
.tl-prod h3{margin-top:18px;font-size:1.15rem;font-weight:800;--z:30px}
.tl-prod p{margin-top:6px;color:var(--tl-muted);font-size:.9rem;line-height:1.5;--z:20px}
.tl-buy{--z:24px;margin-top:16px;width:100%;padding:12px 16px;border:0;border-radius:12px;background:#c2410c;color:#fff;font:inherit;font-weight:700;cursor:pointer;transition:background-color .2s}
.tl-buy:hover{background:#9a3412}
.tl-prod{--tl-focus:#c2410c}

/* feature trio */
.tl-trio{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:22px;list-style:none}
.tl-feat{height:100%;padding:22px;background:#fff;border:1px solid #e2e8f0;box-shadow:0 22px 36px -30px rgba(15,23,42,.5)}
.tl-ico{display:grid;place-items:center;width:46px;height:46px;border-radius:14px;background:#eef2ff;color:#4338ca;--z:30px}
.tl-ico svg{width:24px;height:24px}
.tl-feat h3{margin-top:16px;font-size:1rem;font-weight:800}
.tl-feat p{margin-top:6px;font-size:.88rem;line-height:1.5;color:var(--tl-muted)}
.tl-more{display:inline-block;margin-top:12px;color:var(--tl-accent);font-size:.88rem;font-weight:700;text-decoration:none}
.tl-more:hover{text-decoration:underline}
.tl-sm{--tl-tilt-x:4deg;--tl-tilt-y:-6deg}

@media (prefers-reduced-motion:reduce){
  .tl-card,.tl.is-tilting .tl-card,.tl-glare,.tl-buy{transition:none}
}
JavaScript
/* Tilt: on mouse or pen movement, set --rx / --ry (degrees, up to data-tilt) and --gx / --gy (glare position) on the
   scene. Leaving the card clears them so it eases back flat. Touch and prefers-reduced-motion get no tilt. */
document.querySelectorAll('[data-tilt]').forEach(function (el) {
  var max = parseFloat(el.dataset.tilt) || 12;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  el.addEventListener('pointermove', function (e) {
    if (e.pointerType === 'touch' || reduce.matches) return;
    var r = el.getBoundingClientRect();
    var x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height;
    el.style.setProperty('--rx', ((0.5 - y) * max).toFixed(2) + 'deg');
    el.style.setProperty('--ry', ((x - 0.5) * max).toFixed(2) + 'deg');
    el.style.setProperty('--gx', (x * 100).toFixed(1) + '%');
    el.style.setProperty('--gy', (y * 100).toFixed(1) + '%');
    el.classList.add('is-tilting');
  });
  el.addEventListener('pointerleave', function () {
    ['--rx', '--ry', '--gx', '--gy'].forEach(function (p) { el.style.removeProperty(p); });
    el.classList.remove('is-tilting');
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="row row-cols-1 row-cols-md-2 align-items-start duo">
  <div class="col"><span class="lbl">Event pass with a moving glare</span>
  <div class="tl" data-tilt="16">
    <div class="tl-card tl-pass">
      <div class="tl-glare" aria-hidden="true"></div>
      <div class="d-flex justify-content-between align-items-center tl-z" style="--z:24px"><span class="tl-brand">FIELDNOTES 26</span><span class="badge rounded-pill">Speaker</span></div>
      <p class="tl-holder mt-auto mb-0 tl-z" style="--z:40px">Ama Owusu</p>
      <p class="tl-meta mt-1 mb-0 tl-z" style="--z:30px">Lisbon · 14 to 16 May 2026 · Hall C</p>
      <div class="d-flex justify-content-between align-items-end gap-3 mt-3 tl-z" style="--z:24px"><span class="tl-no">Pass FN-0412</span>
        <a class="btn tl-wallet d-inline-flex align-items-center gap-1" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="6" width="18" height="13" rx="2.5"/><path d="M3 10h18M16 14.5h2"/></svg>Add to wallet</a></div>
    </div>
  </div></div>

  <div class="col"><span class="lbl">Layered product card</span>
  <div class="tl" data-tilt="14">
    <article class="card tl-card tl-prod" aria-labelledby="tl-lamp">
      <div class="card-body">
        <div class="tl-stage">
          <svg class="tl-z" viewBox="0 0 120 140" aria-hidden="true" focusable="false"><ellipse cx="98" cy="74" rx="16" ry="9" transform="rotate(-42 98 74)" fill="#fff7ed" opacity=".9"/><rect x="26" y="122" width="58" height="10" rx="5" fill="#7c2d12"/><path d="M55 122L37 80l38-34" stroke="#9a3412" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="37" cy="80" r="5.5" fill="#7c2d12"/><circle cx="75" cy="46" r="5" fill="#7c2d12"/><path d="M70 41l12-9 30 27-18 19z" fill="#ea580c"/><path d="M82 32l30 27" stroke="#fdba74" stroke-width="2.5" stroke-linecap="round"/></svg>
          <span class="badge rounded-pill tl-price tl-z">€89</span>
        </div>
        <h3 class="card-title mt-3 mb-0 tl-z" id="tl-lamp">Halo desk lamp</h3>
        <p class="card-text mt-2 mb-0 tl-z">Warm dimmable LED with three colour temperatures and a weighted steel base.</p>
        <button class="btn tl-buy w-100 mt-3 tl-z" type="button">Add to cart<span class="visually-hidden">: Halo desk lamp</span></button>
      </div>
    </article>
  </div></div>
</div>

<div><span class="lbl">Feature cards with a gentle tilt</span>
<ul class="row row-cols-1 row-cols-md-3 g-4 list-unstyled mb-0">
  <li class="col"><div class="tl tl-sm h-100" data-tilt="8"><div class="card tl-card tl-feat"><div class="card-body">
    <span class="tl-ico tl-z"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18a4.5 4.5 0 0 1-.9-8.9A6 6 0 0 1 17.7 8 4.5 4.5 0 0 1 17 18"/><path d="M12 12v8M9 17l3 3 3-3"/></svg></span>
    <h3 class="card-title mt-3 mb-0">Offline sync</h3><p class="card-text mt-1 mb-0">Edit on the train. Changes merge the moment you are back online.</p>
    <a class="card-link d-inline-block mt-2" href="#">Learn more<span class="visually-hidden"> about offline sync</span></a></div></div></div></li>
  <li class="col"><div class="tl tl-sm h-100" data-tilt="8"><div class="card tl-card tl-feat"><div class="card-body">
    <span class="tl-ico tl-z"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3M12 14.5v2"/></svg></span>
    <h3 class="card-title mt-3 mb-0">Private by default</h3><p class="card-text mt-1 mb-0">Notes are encrypted on your device before they reach our servers.</p>
    <a class="card-link d-inline-block mt-2" href="#">Learn more<span class="visually-hidden"> about encryption</span></a></div></div></div></li>
  <li class="col"><div class="tl tl-sm h-100" data-tilt="8"><div class="card tl-card tl-feat"><div class="card-body">
    <span class="tl-ico tl-z"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 12a8 8 0 1 0 2.3-5.7L4 8.5"/><path d="M4 4v4.5h4.5M12 8v4.5l3 2"/></svg></span>
    <h3 class="card-title mt-3 mb-0">Version history</h3><p class="card-text mt-1 mb-0">Every save is kept for 90 days, so any page can go back to Tuesday.</p>
    <a class="card-link d-inline-block mt-2" href="#">Learn more<span class="visually-hidden"> about version history</span></a></div></div></div></li>
</ul></div>
CSS
/* 3D tilt cards on Bootstrap .card, .badge and .btn. .tl is the scene (perspective); .tl-card turns by --rx / --ry, which a few
   lines of script set from the pointer position (mouse and pen only, never touch or reduced motion). Keyboard focus inside a
   card gives it a fixed tilt instead. --gx / --gy move the glare. Children with .tl-z sit in front of the card plane. */
.tl{--tl-ink:#0f172a;--tl-muted:#475569;--tl-accent:#4338ca;--tl-focus:#4338ca;--tl-tilt-x:6deg;--tl-tilt-y:-9deg;perspective:900px;color:var(--tl-ink)}
.tl .card{--bs-card-border-radius:22px;--bs-card-border-color:#e2e8f0;--bs-card-color:var(--tl-ink)}
.tl-card{position:relative;border-radius:22px;transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));transform-style:preserve-3d;
  transition:transform .6s cubic-bezier(.2,.8,.2,1),box-shadow .6s;will-change:transform}
.tl.is-tilting .tl-card{transition:transform .08s linear,box-shadow .3s}
.tl:focus-within{--rx:var(--tl-tilt-x);--ry:var(--tl-tilt-y)}
.tl-glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .4s;
  background:radial-gradient(circle at var(--gx,28%) var(--gy,18%),rgba(255,255,255,.5),rgba(255,255,255,0) 52%);mix-blend-mode:soft-light}
.tl.is-tilting .tl-glare,.tl:focus-within .tl-glare{opacity:1}
.tl-z{transform:translateZ(var(--z,36px))}
.tl a:focus-visible,.tl .btn:focus-visible{outline:3px solid var(--tl-focus);outline-offset:3px;box-shadow:none}

/* event pass */
.tl-pass{--tl-focus:#fde68a;max-width:380px;aspect-ratio:1.586;padding:22px 24px;display:flex;flex-direction:column;color:#fff;
  background:repeating-linear-gradient(115deg,rgba(255,255,255,0) 0 14px,rgba(255,255,255,.05) 14px 28px),linear-gradient(135deg,#1e1b4b,#4338ca 55%,#0e7490);
  box-shadow:0 30px 50px -28px rgba(30,27,75,.75)}
.tl-pass .tl-glare{mix-blend-mode:overlay;background:radial-gradient(circle at var(--gx,28%) var(--gy,18%),rgba(255,255,255,.7),rgba(255,255,255,0) 50%),
  linear-gradient(115deg,rgba(244,114,182,0) 30%,rgba(244,114,182,.35) 45%,rgba(56,189,248,.35) 55%,rgba(56,189,248,0) 70%)}
.tl-brand{font-weight:800;letter-spacing:.08em;font-size:.9rem}
.tl-pass .badge{--bs-badge-padding-x:10px;--bs-badge-padding-y:4px;--bs-badge-font-size:.7rem;--bs-badge-font-weight:800;--bs-badge-color:#422006;background:#fde68a;letter-spacing:.1em;text-transform:uppercase}
.tl-holder{font-size:1.5rem;font-weight:800;letter-spacing:-.01em}
.tl-meta{font-size:.86rem;color:#e0e7ff}
.tl-no{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;color:#c7d2fe}
.tl-wallet{--bs-btn-padding-x:14px;--bs-btn-padding-y:8px;--bs-btn-font-size:.82rem;--bs-btn-font-weight:700;--bs-btn-border-radius:999px;--bs-btn-bg:#fff;--bs-btn-color:#1e1b4b;
  --bs-btn-border-color:#fff;--bs-btn-hover-bg:#e0e7ff;--bs-btn-hover-border-color:#e0e7ff;--bs-btn-hover-color:#1e1b4b;--bs-btn-active-bg:#c7d2fe;--bs-btn-active-color:#1e1b4b}
.tl-wallet svg{width:15px;height:15px}

/* layered product card */
.tl-prod{--tl-focus:#c2410c;max-width:340px;box-shadow:0 30px 50px -32px rgba(15,23,42,.5)}
.tl-prod .card-body{padding:18px;transform-style:preserve-3d}
.tl-stage{position:relative;height:190px;border-radius:16px;background:linear-gradient(160deg,#fef3c7,#fdba74);transform-style:preserve-3d;display:grid;place-items:center}
.tl-stage svg{width:132px;height:150px;--z:54px;filter:drop-shadow(0 18px 14px rgba(124,45,18,.28))}
.tl-price{position:absolute;top:14px;right:14px;--z:70px;--bs-badge-padding-x:12px;--bs-badge-padding-y:6px;--bs-badge-font-size:.9rem;--bs-badge-font-weight:800;background:#0f172a}
.tl-prod .card-title{--z:30px;font-size:1.15rem;font-weight:800}
.tl-prod .card-text{--z:20px;color:var(--tl-muted);font-size:.9rem;line-height:1.5}
.tl-buy{--z:24px;--bs-btn-padding-y:12px;--bs-btn-font-weight:700;--bs-btn-border-radius:12px;--bs-btn-bg:#c2410c;--bs-btn-border-color:#c2410c;--bs-btn-color:#fff;
  --bs-btn-hover-bg:#9a3412;--bs-btn-hover-border-color:#9a3412;--bs-btn-hover-color:#fff;--bs-btn-active-bg:#7c2d12;--bs-btn-active-border-color:#7c2d12;--bs-btn-active-color:#fff}

/* feature cards */
.tl-feat{height:100%;box-shadow:0 22px 36px -30px rgba(15,23,42,.5)}
.tl-feat .card-body{padding:22px;transform-style:preserve-3d}
.tl-ico{display:grid;place-items:center;width:46px;height:46px;border-radius:14px;background:#eef2ff;color:#4338ca;--z:30px}
.tl-ico svg{width:24px;height:24px}
.tl-feat .card-title{font-size:1rem;font-weight:800}
.tl-feat .card-text{font-size:.88rem;line-height:1.5;color:var(--tl-muted)}
.tl-feat .card-link{color:var(--tl-accent);font-size:.88rem;font-weight:700;text-decoration:none}
.tl-feat .card-link:hover{text-decoration:underline}
.tl-sm{--tl-tilt-x:4deg;--tl-tilt-y:-6deg}

@media (prefers-reduced-motion:reduce){
  .tl-card,.tl.is-tilting .tl-card,.tl-glare,.tl .btn{transition:none}
}
JavaScript
/* Tilt: on mouse or pen movement, set --rx / --ry (degrees, up to data-tilt) and --gx / --gy (glare position) on the
   scene. Leaving the card clears them so it eases back flat. Touch and prefers-reduced-motion get no tilt. */
document.querySelectorAll('[data-tilt]').forEach(function (el) {
  var max = parseFloat(el.dataset.tilt) || 12;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  el.addEventListener('pointermove', function (e) {
    if (e.pointerType === 'touch' || reduce.matches) return;
    var r = el.getBoundingClientRect();
    var x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height;
    el.style.setProperty('--rx', ((0.5 - y) * max).toFixed(2) + 'deg');
    el.style.setProperty('--ry', ((x - 0.5) * max).toFixed(2) + 'deg');
    el.style.setProperty('--gx', (x * 100).toFixed(1) + '%');
    el.style.setProperty('--gy', (y * 100).toFixed(1) + '%');
    el.classList.add('is-tilting');
  });
  el.addEventListener('pointerleave', function () {
    ['--rx', '--ry', '--gx', '--gy'].forEach(function (p) { el.style.removeProperty(p); });
    el.classList.remove('is-tilting');
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,300px),1fr))] items-start gap-10">
  <div><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-[#475569] uppercase">Event pass with a moving glare</span>
  <div class="tl group perspective-[900px] focus-within:[--rx:6deg] focus-within:[--ry:-9deg]" data-tilt="16">
    <div class="tl-turn relative flex aspect-[1.586] max-w-[380px] flex-col rounded-[22px] bg-[repeating-linear-gradient(115deg,rgba(255,255,255,0)_0_14px,rgba(255,255,255,.05)_14px_28px),linear-gradient(135deg,#1e1b4b,#4338ca_55%,#0e7490)] px-6 py-[22px] text-white shadow-[0_30px_50px_-28px_rgba(30,27,75,.75)] transition-[transform,box-shadow] duration-600 ease-[cubic-bezier(.2,.8,.2,1)] will-change-transform group-[.is-tilting]:duration-[80ms] group-[.is-tilting]:ease-linear motion-reduce:transition-none">
      <div class="tl-holo pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 mix-blend-overlay transition-opacity duration-400 group-focus-within:opacity-100 group-[.is-tilting]:opacity-100 motion-reduce:transition-none" aria-hidden="true"></div>
      <div class="flex items-center justify-between [transform:translateZ(24px)]"><span class="text-[.9rem] font-extrabold tracking-[.08em]">FIELDNOTES 26</span><span class="rounded-full bg-[#fde68a] px-2.5 py-1 text-[.7rem] font-extrabold tracking-[.1em] text-[#422006] uppercase">Speaker</span></div>
      <p class="mt-auto text-2xl font-extrabold tracking-[-.01em] [transform:translateZ(40px)]">Ama Owusu</p>
      <p class="mt-1 text-[.86rem] text-[#e0e7ff] [transform:translateZ(30px)]">Lisbon · 14 to 16 May 2026 · Hall C</p>
      <div class="mt-4 flex items-end justify-between gap-3 [transform:translateZ(24px)]"><span class="font-mono text-[.78rem] text-[#c7d2fe]">Pass FN-0412</span>
        <a class="inline-flex items-center gap-1.5 rounded-full bg-white px-3.5 py-2 text-[.82rem] font-bold text-[#1e1b4b] no-underline hover:bg-[#e0e7ff] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#fde68a]" href="#"><svg class="size-[15px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="6" width="18" height="13" rx="2.5"/><path d="M3 10h18M16 14.5h2"/></svg>Add to wallet</a></div>
    </div>
  </div></div>

  <div><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-[#475569] uppercase">Layered product card</span>
  <div class="tl group perspective-[900px] focus-within:[--rx:6deg] focus-within:[--ry:-9deg]" data-tilt="14">
    <article class="tl-turn relative max-w-[340px] rounded-[22px] border border-[#e2e8f0] bg-white p-[18px] shadow-[0_30px_50px_-32px_rgba(15,23,42,.5)] transition-[transform,box-shadow] duration-600 ease-[cubic-bezier(.2,.8,.2,1)] will-change-transform group-[.is-tilting]:duration-[80ms] group-[.is-tilting]:ease-linear motion-reduce:transition-none" aria-labelledby="tlw-lamp">
      <div class="relative grid h-[190px] place-items-center rounded-2xl bg-[linear-gradient(160deg,#fef3c7,#fdba74)] transform-3d">
        <svg class="h-[150px] w-[132px] drop-shadow-[0_18px_14px_rgba(124,45,18,.28)] [transform:translateZ(54px)]" viewBox="0 0 120 140" aria-hidden="true" focusable="false"><ellipse cx="98" cy="74" rx="16" ry="9" transform="rotate(-42 98 74)" fill="#fff7ed" opacity=".9"/><rect x="26" y="122" width="58" height="10" rx="5" fill="#7c2d12"/><path d="M55 122L37 80l38-34" stroke="#9a3412" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="37" cy="80" r="5.5" fill="#7c2d12"/><circle cx="75" cy="46" r="5" fill="#7c2d12"/><path d="M70 41l12-9 30 27-18 19z" fill="#ea580c"/><path d="M82 32l30 27" stroke="#fdba74" stroke-width="2.5" stroke-linecap="round"/></svg>
        <span class="absolute top-3.5 right-3.5 rounded-full bg-[#0f172a] px-3 py-1.5 text-[.9rem] font-extrabold text-white [transform:translateZ(70px)]">€89</span>
      </div>
      <h3 class="mt-[18px] text-[1.15rem] font-extrabold [transform:translateZ(30px)]" id="tlw-lamp">Halo desk lamp</h3>
      <p class="mt-1.5 text-[.9rem] leading-normal text-[#475569] [transform:translateZ(20px)]">Warm dimmable LED with three colour temperatures and a weighted steel base.</p>
      <button class="mt-4 w-full cursor-pointer rounded-xl bg-[#c2410c] px-4 py-3 font-bold text-white transition-colors duration-200 [transform:translateZ(24px)] hover:bg-[#9a3412] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#c2410c] motion-reduce:transition-none" type="button">Add to cart<span class="sr-only">: Halo desk lamp</span></button>
    </article>
  </div></div>
</div>

<div><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-[#475569] uppercase">Feature cards with a gentle tilt</span>
<ul class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,220px),1fr))] gap-[22px]">
  <li class="tl group perspective-[900px] focus-within:[--rx:4deg] focus-within:[--ry:-6deg]" data-tilt="8"><div class="tl-turn h-full rounded-[22px] border border-[#e2e8f0] bg-white p-[22px] shadow-[0_22px_36px_-30px_rgba(15,23,42,.5)] transition-[transform,box-shadow] duration-600 ease-[cubic-bezier(.2,.8,.2,1)] group-[.is-tilting]:duration-[80ms] group-[.is-tilting]:ease-linear motion-reduce:transition-none">
    <span class="grid size-[46px] place-items-center rounded-[14px] bg-[#eef2ff] text-[#4338ca] [transform:translateZ(30px)]"><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18a4.5 4.5 0 0 1-.9-8.9A6 6 0 0 1 17.7 8 4.5 4.5 0 0 1 17 18"/><path d="M12 12v8M9 17l3 3 3-3"/></svg></span>
    <h3 class="mt-4 text-base font-extrabold">Offline sync</h3><p class="mt-1.5 text-[.88rem] leading-normal text-[#475569]">Edit on the train. Changes merge the moment you are back online.</p>
    <a class="mt-3 inline-block text-[.88rem] font-bold text-[#4338ca] no-underline hover:underline focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4338ca]" href="#">Learn more<span class="sr-only"> about offline sync</span></a></div></li>
  <li class="tl group perspective-[900px] focus-within:[--rx:4deg] focus-within:[--ry:-6deg]" data-tilt="8"><div class="tl-turn h-full rounded-[22px] border border-[#e2e8f0] bg-white p-[22px] shadow-[0_22px_36px_-30px_rgba(15,23,42,.5)] transition-[transform,box-shadow] duration-600 ease-[cubic-bezier(.2,.8,.2,1)] group-[.is-tilting]:duration-[80ms] group-[.is-tilting]:ease-linear motion-reduce:transition-none">
    <span class="grid size-[46px] place-items-center rounded-[14px] bg-[#eef2ff] text-[#4338ca] [transform:translateZ(30px)]"><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3M12 14.5v2"/></svg></span>
    <h3 class="mt-4 text-base font-extrabold">Private by default</h3><p class="mt-1.5 text-[.88rem] leading-normal text-[#475569]">Notes are encrypted on your device before they reach our servers.</p>
    <a class="mt-3 inline-block text-[.88rem] font-bold text-[#4338ca] no-underline hover:underline focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4338ca]" href="#">Learn more<span class="sr-only"> about encryption</span></a></div></li>
  <li class="tl group perspective-[900px] focus-within:[--rx:4deg] focus-within:[--ry:-6deg]" data-tilt="8"><div class="tl-turn h-full rounded-[22px] border border-[#e2e8f0] bg-white p-[22px] shadow-[0_22px_36px_-30px_rgba(15,23,42,.5)] transition-[transform,box-shadow] duration-600 ease-[cubic-bezier(.2,.8,.2,1)] group-[.is-tilting]:duration-[80ms] group-[.is-tilting]:ease-linear motion-reduce:transition-none">
    <span class="grid size-[46px] place-items-center rounded-[14px] bg-[#eef2ff] text-[#4338ca] [transform:translateZ(30px)]"><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 12a8 8 0 1 0 2.3-5.7L4 8.5"/><path d="M4 4v4.5h4.5M12 8v4.5l3 2"/></svg></span>
    <h3 class="mt-4 text-base font-extrabold">Version history</h3><p class="mt-1.5 text-[.88rem] leading-normal text-[#475569]">Every save is kept for 90 days, so any page can go back to Tuesday.</p>
    <a class="mt-3 inline-block text-[.88rem] font-bold text-[#4338ca] no-underline hover:underline focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4338ca]" href="#">Learn more<span class="sr-only"> about version history</span></a></div></li>
</ul></div>
CSS
/* The tilt itself: the scene (.tl) carries --rx / --ry from the script or from keyboard focus; the card turns by them and
   the glare follows --gx / --gy. Depth layers use [transform:translateZ()] utilities in the markup. */
@utility tl-turn{transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));transform-style:preserve-3d}
@utility tl-glare{background:radial-gradient(circle at var(--gx,28%) var(--gy,18%),rgba(255,255,255,.5),rgba(255,255,255,0) 52%)}
@utility tl-holo{background:radial-gradient(circle at var(--gx,28%) var(--gy,18%),rgba(255,255,255,.7),rgba(255,255,255,0) 50%),
  linear-gradient(115deg,rgba(244,114,182,0) 30%,rgba(244,114,182,.35) 45%,rgba(56,189,248,.35) 55%,rgba(56,189,248,0) 70%)}
JavaScript
/* Tilt: on mouse or pen movement, set --rx / --ry (degrees, up to data-tilt) and --gx / --gy (glare position) on the
   scene. Leaving the card clears them so it eases back flat. Touch and prefers-reduced-motion get no tilt. */
document.querySelectorAll('[data-tilt]').forEach(function (el) {
  var max = parseFloat(el.dataset.tilt) || 12;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  el.addEventListener('pointermove', function (e) {
    if (e.pointerType === 'touch' || reduce.matches) return;
    var r = el.getBoundingClientRect();
    var x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height;
    el.style.setProperty('--rx', ((0.5 - y) * max).toFixed(2) + 'deg');
    el.style.setProperty('--ry', ((x - 0.5) * max).toFixed(2) + 'deg');
    el.style.setProperty('--gx', (x * 100).toFixed(1) + '%');
    el.style.setProperty('--gy', (y * 100).toFixed(1) + '%');
    el.classList.add('is-tilting');
  });
  el.addEventListener('pointerleave', function () {
    ['--rx', '--ry', '--gx', '--gy'].forEach(function (p) { el.style.removeProperty(p); });
    el.classList.remove('is-tilting');
  });
});

About this set

Cards that lean toward the pointer, in three variants. The event pass is a conference badge with a holographic stripe pattern and a glare that follows the pointer across its surface. The layered product card puts the illustration, price tag, title and button on separate planes in front of the card, so they shift against each other as it tilts. The feature cards are a row of three smaller cards with a gentler tilt for landing pages. Each scene sets a perspective, and the card turns by two custom properties, –rx and –ry, which a short vanilla script sets from the pointer position on mouse and pen movement; touch input and visitors who prefer reduced motion get no tilt. Depth layers sit in front of the card with translateZ inside a preserve-3d card. When a link or button inside a card gets keyboard focus, :focus-within gives the card a fixed tilt and shows the glare, so keyboard users see the effect too. Without JavaScript the cards are ordinary flat cards and everything still works. Use the pass for tickets and memberships, the layered card for product highlights and the trio for feature grids.

What’s included

  • Pointer-driven tilt with a moving glare from about 20 lines of vanilla JavaScript
  • Fixed tilt on keyboard focus through :focus-within
  • Depth layers placed with translateZ inside a preserve-3d card
  • No tilt for touch input or prefers-reduced-motion
  • Flat, fully usable cards when JavaScript is off

Accessibility

The tilt is decoration only: every card keeps real headings, text and a named link or button, such as Add to cart: Halo desk lamp. Tab reaches each control, which shows a 3px focus outline, and focus inside a card gives it a fixed tilt instead of following a pointer. The script ignores touch input and prefers-reduced-motion, and the transitions are removed under reduced motion.

Customise it

Set the maximum tilt per card with data-tilt, the keyboard focus tilt with --tl-tilt-x and --tl-tilt-y, and the depth of each layer with --z (plain CSS and Bootstrap). Colours sit on .tl-pass, .tl-prod and .tl-ico. In Tailwind the same values are focus-within:[--rx] and focus-within:[--ry] utilities and [transform:translateZ()] classes.