3D tilt card hover effect

A card that turns to face your pointer as it moves, with a glare that follows. Move your mouse over it to try.

UI loop

Cards that tilt toward you

Issue 04 Cards that tilt toward you
Sundry Millsundrymill.com/library
3D tilt card hover effect

Everything in the preview is live. Try it.

The code

Tilt card

The card turns to face your pointer and a soft glare follows it.

HTML
<div class="tilt" data-tilt>
  <div class="tilt-card">
    <span class="tilt-kicker">Issue 04</span>
    <strong class="tilt-title">Cards that tilt toward you</strong>
    <span class="tilt-glare" aria-hidden="true"></span>
  </div>
</div>
CSS
.tilt { width: min(100%, 260px); perspective: 900px; }
.tilt-card {
  position: relative;
  aspect-ratio: 3 / 4;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 6px;
  padding: 22px;
  border-radius: 20px;
  overflow: hidden;
  color: #FFFFFF;
  background:
    radial-gradient(120% 90% at 15% 5%, #FFB23F 0%, transparent 55%),
    radial-gradient(100% 80% at 95% 100%, #2F55D4 0%, transparent 60%),
    linear-gradient(160deg, #FF7A2F, #7A3450);
  box-shadow: 0 30px 50px -30px rgba(23, 32, 44, .7);
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform .5s cubic-bezier(.05, .7, .1, 1);
}
.tilt.is-moving .tilt-card { transition: transform .08s linear; }
.tilt-kicker { font-size: .8rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.tilt-title { font-size: 1.5rem; line-height: 1.1; }
.tilt-glare {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at var(--gx, 50%) var(--gy, 50%), rgba(255, 255, 255, .45), transparent 55%);
  opacity: var(--go, 0);
  transition: opacity .3s;
  pointer-events: none;
}
JavaScript
const TILT = 12; // degrees at the edge
document.querySelectorAll('[data-tilt]').forEach((wrap) => {
  const card = wrap.firstElementChild;
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  wrap.addEventListener('pointermove', (e) => {
    const r = card.getBoundingClientRect();
    const x = (e.clientX - r.left) / r.width;
    const y = (e.clientY - r.top) / r.height;
    wrap.classList.add('is-moving');
    card.style.setProperty('--ry', ((x - 0.5) * 2 * TILT).toFixed(2) + 'deg');
    card.style.setProperty('--rx', ((0.5 - y) * 2 * TILT).toFixed(2) + 'deg');
    card.style.setProperty('--gx', (x * 100).toFixed(1) + '%');
    card.style.setProperty('--gy', (y * 100).toFixed(1) + '%');
    card.style.setProperty('--go', 1);
  });
  wrap.addEventListener('pointerleave', () => {
    wrap.classList.remove('is-moving');
    ['--rx', '--ry', '--go'].forEach((p) => card.style.removeProperty(p));
  });
});

How to use it

  • Keep the tilt small. Around 12 degrees at the edge feels solid; 30 feels like the card is falling over.
  • The script does nothing when someone has asked for reduced motion, and touch screens only tilt while a finger is on the card.
  • Use it for one or two feature cards, not a grid of forty.

Showing an interaction like this to a client or in a tutorial? Framok records your screen and zooms in on every click for you.

Free to use in personal and commercial work. No credit needed, though a link back is always nice.

More from the library

See the whole library