Components / Motion / Tilt Card

Tilt Card beta

A card that tips toward the cursor in 3D, with a glare that follows and a layer that floats.

Preview

Northwind Pro

The whole workspace, one payment. This card is the part — tip it.

SeatsUnlimited
UpdatesLifetime
Price$129 once
<div style="display:flex;justify-content:center;padding:10px 0">
  <div class="k-tilt" data-tilt>
    <div class="k-tilt-in">
      <div class="pop">
        <h3>Northwind Pro</h3>
        <p style="margin:7px 0 14px">The whole workspace, one payment. This card is the part — tip it.</p>
        <div class="row"><span>Seats</span><b>Unlimited</b></div>
        <div class="row"><span>Updates</span><b>Lifetime</b></div>
        <div class="row"><span>Price</span><b>$129 once</b></div>
      </div>
    </div>
  </div>
</div>
/* KATA Tilt Card — perspective, a glare, one floating layer. */
.k-tilt{perspective:900px;font-family:var(--k-font);max-width:330px}
.k-tilt-in{position:relative;background:var(--k-zinc-950);color:var(--k-on-color);border-radius:16px;padding:22px;transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));transition:transform .3s ease-out;transform-style:preserve-3d;overflow:hidden}
.k-tilt.live .k-tilt-in{transition:transform .08s linear}
.k-tilt-in::after{content:"";position:absolute;inset:0;background:radial-gradient(340px circle at var(--gx,50%) var(--gy,50%), rgba(255,255,255,.14), transparent 65%);pointer-events:none}
.k-tilt .pop{transform:translateZ(34px)}
.k-tilt h3{font-size:17px;font-weight:650;letter-spacing:-.015em}
.k-tilt p{font-size:12.5px;line-height:1.75;color:var(--k-zinc-400)}
.k-tilt .row{display:flex;justify-content:space-between;font-size:12.5px;padding:8px 0;border-top:1px solid var(--k-zinc-800);color:var(--k-zinc-300)}
// KATA Tilt Card — a few degrees, a whisper of glare, then home.
document.querySelectorAll('[data-tilt]').forEach(function (wrap) {
  var card = wrap.querySelector('.k-tilt-in');
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  wrap.addEventListener('pointermove', function (e) {
    var r = wrap.getBoundingClientRect();
    var px = (e.clientX - r.left) / r.width;
    var py = (e.clientY - r.top) / r.height;
    wrap.classList.add('live');
    card.style.setProperty('--ry', ((px - .5) * 13).toFixed(2) + 'deg');
    card.style.setProperty('--rx', ((.5 - py) * 11).toFixed(2) + 'deg');
    card.style.setProperty('--gx', (px * 100).toFixed(1) + '%');
    card.style.setProperty('--gy', (py * 100).toFixed(1) + '%');
  });
  wrap.addEventListener('pointerleave', function () {
    wrap.classList.remove('live');
    card.style.setProperty('--rx', '0deg');
    card.style.setProperty('--ry', '0deg');
  });
});

Installation

CLI

node kata-ui.mjs add tilt-card

Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Usage

One card per view earns this — a featured plan, a product shot, the thing being sold. The tilt caps at a few degrees, the glare is a whisper, and an inner layer floats on translateZ so the depth is real. The moment everything on the page tilts, nothing does.

Accessibility

The tilt is pointer-only garnish on a static card: content, order and actions are identical with the script removed, and touch and keyboard never trigger it. Under prefers-reduced-motion the card stays flat, which suits it fine.

Anatomy

The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.

.k-tiltclass
.k-tilt-inclass
.liveclass
.popclass
.rowclass
data-tiltdata hook

Related parts