3D tilt card
The card tilts toward the pointer in 3D, with a soft glare that follows the cursor.
CSSJavaScriptIntermediate
<div class="scene"><div class="card" id="card"><div class="glare"></div><h2>Tilt me</h2><p>Move your cursor over the card</p></div></div>
.scene{perspective:800px}
.card{position:relative;width:min(260px,76vw);aspect-ratio:3/4;border-radius:18px;padding:24px;display:flex;flex-direction:column;justify-content:flex-end;
background:linear-gradient(150deg,#3a1510,#9c4528 55%,#f08a5d);color:#fff;transform-style:preserve-3d;
transition:transform .5s cubic-bezier(.2,.8,.2,1);box-shadow:0 24px 60px -20px rgba(0,0,0,.7)}
.card.live{transition:transform .08s linear}
.card h2{margin:0;font-size:1.8rem;transform:translateZ(40px)}
.card p{margin:4px 0 0;opacity:.8;transform:translateZ(24px)}
.glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .3s;
background:radial-gradient(circle at var(--gx,50%) var(--gy,50%),rgba(255,255,255,.35),transparent 55%)}
.card:hover .glare{opacity:1}
const card = document.getElementById('card');
const max = 10; // degrees
if (matchMedia('(hover:hover) and (pointer:fine)').matches) {
card.addEventListener('pointermove', e => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width; // 0 to 1
const py = (e.clientY - r.top) / r.height;
card.classList.add('live');
card.style.transform = `rotateY(${(px - .5) * max * 2}deg) rotateX(${(.5 - py) * max * 2}deg) scale(1.03)`;
card.style.setProperty('--gx', px * 100 + '%');
card.style.setProperty('--gy', py * 100 + '%');
});
card.addEventListener('pointerleave', () => {
card.classList.remove('live');
card.style.transform = '';
});
}
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>3D tilt card</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
.scene{perspective:800px}
.card{position:relative;width:min(260px,76vw);aspect-ratio:3/4;border-radius:18px;padding:24px;display:flex;flex-direction:column;justify-content:flex-end;
background:linear-gradient(150deg,#3a1510,#9c4528 55%,#f08a5d);color:#fff;transform-style:preserve-3d;
transition:transform .5s cubic-bezier(.2,.8,.2,1);box-shadow:0 24px 60px -20px rgba(0,0,0,.7)}
.card.live{transition:transform .08s linear}
.card h2{margin:0;font-size:1.8rem;transform:translateZ(40px)}
.card p{margin:4px 0 0;opacity:.8;transform:translateZ(24px)}
.glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .3s;
background:radial-gradient(circle at var(--gx,50%) var(--gy,50%),rgba(255,255,255,.35),transparent 55%)}
.card:hover .glare{opacity:1}
</style></head>
<body>
<div class="scene"><div class="card" id="card"><div class="glare"></div><h2>Tilt me</h2><p>Move your cursor over the card</p></div></div>
<script>
const card = document.getElementById('card');
const max = 10; // degrees
if (matchMedia('(hover:hover) and (pointer:fine)').matches) {
card.addEventListener('pointermove', e => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width; // 0 to 1
const py = (e.clientY - r.top) / r.height;
card.classList.add('live');
card.style.transform = `rotateY(${(px - .5) * max * 2}deg) rotateX(${(.5 - py) * max * 2}deg) scale(1.03)`;
card.style.setProperty('--gx', px * 100 + '%');
card.style.setProperty('--gy', py * 100 + '%');
});
card.addEventListener('pointerleave', () => {
card.classList.remove('live');
card.style.transform = '';
});
}
</script>
</body></html>
Good for
Product cards, player cards, pricing tiles and trophies.
Watch out for
Keep the tilt under about 10 degrees. Reset smoothly on leave and skip it on touch devices.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.