<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');
});
});
/* 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');
});
});
/* 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.