SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/tilt-cards

7 CSS 3D Tilt Hover Cards

4 Pure CSS3 with JSPublished 2026-10-05Free · no sign-up

Seven ways to give a card depth: a tilt that follows the pointer, a script-free tilt from hover zones, a moving glare, layers at different depths, a payment card with a sheen, a tilt that also runs on keyboard focus and a card that floats above its own shadow.

01. Card That Tilts Toward the Pointer

3DJavaScript

The card rotates on two axes to face the pointer, with perspective on the parent. Script converts the pointer position into two angles of at most 10 degrees, stored in custom properties, and the card eases back flat when the pointer leaves. It does nothing for reduced motion.

View code
<div class="tc1"><article class="card"><p class="k">New</p><h3>Aurora headphones</h3><p>40 hours of battery, folding frame.</p></article></div>
.tc1 { perspective: 700px; }
.tc1 .card { --rx: 0deg; --ry: 0deg; width: 250px; max-width: 100%; box-sizing: border-box; padding: 24px; border-radius: 20px; background: linear-gradient(145deg, #1e293b, #0f172a); border: 1px solid #334155; color: #e2e8f0; font-family: Inter, system-ui, sans-serif; transform: rotateX(var(--rx)) rotateY(var(--ry)); transition: transform .4s ease; box-shadow: 0 30px 50px -30px #000; }
.tc1 .card.live { transition: transform .08s linear; }
.tc1 .k { margin: 0; color: #38bdf8; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; }
.tc1 h3 { margin: 6px 0; color: #fff; font-size: 20px; } .tc1 p:last-child { margin: 0; color: #94a3b8; font-size: 14px; }
var c = document.querySelector('.tc1 .card');
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
  c.addEventListener('pointermove', function (e) {
    var r = c.getBoundingClientRect(), x = (e.clientX - r.left) / r.width - .5, y = (e.clientY - r.top) / r.height - .5;
    c.classList.add('live');
    c.style.setProperty('--ry', x * 20 + 'deg'); c.style.setProperty('--rx', -y * 20 + 'deg');
  });
  c.addEventListener('pointerleave', function () { c.classList.remove('live'); c.style.setProperty('--rx', '0deg'); c.style.setProperty('--ry', '0deg'); });
}

02. CSS-Only Tilt with Hover Zones

3DNo JS

No script: nine invisible zones cover the card in a grid, and hovering a zone tilts the card toward it with :has(). It is a useful trick when you cannot add JavaScript. The zones are empty elements hidden from assistive technology.

View code
<div class="tc2"><div class="z" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div><article><h3>Hover the corners</h3><p>The card leans toward the zone under the pointer.</p></article></div>
.tc2 { position: relative; width: 250px; max-width: 100%; height: 170px; perspective: 600px; font-family: Inter, system-ui, sans-serif; }
.tc2 article { position: absolute; inset: 0; box-sizing: border-box; padding: 22px; border-radius: 18px; background: #fff; border: 1px solid #e4e7ec; box-shadow: 0 20px 40px -24px rgba(16,24,40,.45); transition: transform .3s ease; pointer-events: none; }
.tc2 h3 { margin: 0 0 6px; color: #101828; } .tc2 p { margin: 0; color: #475467; font-size: 14px; }
.tc2 .z { position: absolute; inset: 0; display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr); z-index: 2; }
.tc2:has(i:nth-child(1):hover) article { transform: rotateX(8deg) rotateY(-8deg); }
.tc2:has(i:nth-child(2):hover) article { transform: rotateX(8deg); }
.tc2:has(i:nth-child(3):hover) article { transform: rotateX(8deg) rotateY(8deg); }
.tc2:has(i:nth-child(4):hover) article { transform: rotateY(-8deg); }
.tc2:has(i:nth-child(6):hover) article { transform: rotateY(8deg); }
.tc2:has(i:nth-child(7):hover) article { transform: rotateX(-8deg) rotateY(-8deg); }
.tc2:has(i:nth-child(8):hover) article { transform: rotateX(-8deg); }
.tc2:has(i:nth-child(9):hover) article { transform: rotateX(-8deg) rotateY(8deg); }
@media (prefers-reduced-motion: reduce) { .tc2 article { transform: none !important; } }

03. Tilt with Moving Glare

3DLight

On top of the tilt, a soft white glare moves to the side nearest the pointer, as if light were catching a glossy surface. The glare is a radial gradient on a pseudo-element positioned by the same custom properties the tilt uses.

View code
<div class="tc3"><article class="card"><h3>Night pass</h3><p>Valid 18:00 to 06:00</p></article></div>
.tc3 { perspective: 800px; }
.tc3 .card { --rx: 0deg; --ry: 0deg; --gx: 50%; --gy: 50%; position: relative; width: 250px; max-width: 100%; aspect-ratio: 16 / 10; box-sizing: border-box; padding: 22px; border-radius: 18px; background: linear-gradient(135deg, #4338ca, #9333ea); color: #fff; font-family: Inter, system-ui, sans-serif; overflow: hidden; transform: rotateX(var(--rx)) rotateY(var(--ry)); transition: transform .3s ease; }
.tc3 .card::after { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at var(--gx) var(--gy), rgba(255,255,255,.35), transparent 55%); opacity: 0; transition: opacity .3s; pointer-events: none; }
.tc3 .card:hover::after { opacity: 1; }
.tc3 h3 { margin: 0 0 6px; font-size: 22px; } .tc3 p { margin: 0; color: #e9d5ff; }
var c = document.querySelector('.tc3 .card'), still = matchMedia('(prefers-reduced-motion: reduce)').matches;
c.addEventListener('pointermove', function (e) {
  var r = c.getBoundingClientRect(), x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height;
  c.style.setProperty('--gx', x * 100 + '%'); c.style.setProperty('--gy', y * 100 + '%');
  if (!still) { c.style.setProperty('--ry', (x - .5) * 16 + 'deg'); c.style.setProperty('--rx', (.5 - y) * 16 + 'deg'); }
});
c.addEventListener('pointerleave', function () { c.style.setProperty('--rx', '0deg'); c.style.setProperty('--ry', '0deg'); });

04. Layered Depth on Hover

3DParallax

Elements inside the card sit at different depths with translateZ inside a preserve-3d parent, so when the card tilts on hover the badge and title move more than the background. It gives a pop-up-book feeling with a single fixed rotation and no script.

View code
<div class="tc4"><article class="card"><span class="badge">-30%</span><h3>Linen shirt</h3><p>Summer sale ends Sunday</p></article></div>
.tc4 { perspective: 800px; }
.tc4 .card { position: relative; width: 250px; max-width: 100%; height: 170px; box-sizing: border-box; padding: 24px; border-radius: 18px; background: linear-gradient(150deg, #fef3c7, #fde68a); transform-style: preserve-3d; transition: transform .45s ease; font-family: Inter, system-ui, sans-serif; }
.tc4 .card:hover { transform: rotateX(10deg) rotateY(-14deg); }
.tc4 h3 { margin: 30px 0 4px; color: #451a03; font-size: 22px; transform: translateZ(40px); }
.tc4 p { margin: 0; color: #78350f; transform: translateZ(20px); }
.tc4 .badge { position: absolute; top: 16px; right: 16px; padding: 4px 10px; border-radius: 999px; background: #b91c1c; color: #fff; font-weight: 800; font-size: 13px; transform: translateZ(60px); }
@media (prefers-reduced-motion: reduce) { .tc4 .card { transition: none; } .tc4 .card:hover { transform: none; } }

05. Payment Card with Tilt and Sheen

3DCard

A credit-card-sized mock-up that tilts toward the pointer and shows a diagonal sheen. The number is fake and shortened to the last four digits, which is also what you should show on a real account page. aspect-ratio keeps the 85.6 by 54 mm card proportions at any width.

View code
<div class="tc5"><div class="cc" role="img" aria-label="Card ending 4821"><span class="chip"></span><span class="num">•••• 4821</span><span class="nm">A. MENDES</span></div></div>
.tc5 { perspective: 900px; }
.tc5 .cc { --rx: 0deg; --ry: 0deg; position: relative; display: grid; grid-template-rows: 1fr auto auto; width: 280px; max-width: 100%; aspect-ratio: 85.6 / 54; box-sizing: border-box; padding: 20px; border-radius: 16px; background: linear-gradient(125deg, #0f766e, #134e4a 60%, #042f2e); color: #ecfeff; font-family: "JetBrains Mono", ui-monospace, monospace; overflow: hidden; transform: rotateX(var(--rx)) rotateY(var(--ry)); transition: transform .25s ease; box-shadow: 0 24px 40px -24px #000; }
.tc5 .cc::after { content: ""; position: absolute; inset: -50%; background: linear-gradient(115deg, transparent 45%, rgba(255,255,255,.18) 50%, transparent 55%); transform: translateX(calc(var(--ry) * 4)); pointer-events: none; }
.tc5 .chip { width: 38px; height: 28px; border-radius: 6px; background: linear-gradient(135deg, #fde68a, #ca8a04); }
.tc5 .num { font-size: 18px; letter-spacing: .12em; } .tc5 .nm { font-size: 12px; letter-spacing: .14em; opacity: .85; }
var c = document.querySelector('.tc5 .cc');
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
  c.addEventListener('pointermove', function (e) {
    var r = c.getBoundingClientRect();
    c.style.setProperty('--ry', ((e.clientX - r.left) / r.width - .5) * 18 + 'deg');
    c.style.setProperty('--rx', (.5 - (e.clientY - r.top) / r.height) * 18 + 'deg');
  });
  c.addEventListener('pointerleave', function () { c.style.setProperty('--rx', '0deg'); c.style.setProperty('--ry', '0deg'); });
}

06. Tilt on Keyboard Focus Too

3DAccessibility

Three linked cards that tilt slightly when hovered or focused, so keyboard users get the same feedback as mouse users. The tilt is a fixed CSS rotation on :hover and :focus-visible, which needs no script and is easy to switch off for reduced motion.

View code
<ul class="tc6"><li><a href="#design">Design</a></li><li><a href="#build">Build</a></li><li><a href="#launch">Launch</a></li></ul>
.tc6 { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin: 0; padding: 0; list-style: none; perspective: 600px; box-sizing: border-box; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.tc6 a { display: grid; place-items: center; width: 90px; height: 110px; border: 1px solid #d0d5dd; border-radius: 14px; background: #fff; color: #101828; font-weight: 800; text-decoration: none; transition: transform .3s ease, box-shadow .3s ease; }
.tc6 a:hover, .tc6 a:focus-visible { transform: rotateY(-14deg) rotateX(6deg) translateZ(10px); box-shadow: 10px 16px 24px -14px rgba(16,24,40,.45); }
.tc6 a:focus-visible { outline: 3px solid #528bff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .tc6 a { transition: none; } .tc6 a:hover, .tc6 a:focus-visible { transform: none; } }

07. Floating Card with Ground Shadow

3DShadow

The card floats slowly up and down while a separate shadow ellipse beneath it shrinks and softens in step, which sells the height far better than a box-shadow alone. Both use the same duration and easing so they stay in sync.

View code
<div class="tc7"><article><h3>Pro plan</h3><p>Everything in Team, plus audit logs.</p></article><span class="sh" aria-hidden="true"></span></div>
.tc7 { display: grid; justify-items: center; gap: 22px; font-family: Inter, system-ui, sans-serif; }
.tc7 article { width: 230px; max-width: 100%; box-sizing: border-box; padding: 22px; border-radius: 18px; background: linear-gradient(160deg, #312e81, #1e1b4b); border: 1px solid #4338ca; color: #e0e7ff; animation: tc7 3.2s ease-in-out infinite; }
.tc7 h3 { margin: 0 0 6px; color: #fff; } .tc7 p { margin: 0; font-size: 14px; color: #c7d2fe; }
.tc7 .sh { width: 180px; height: 16px; border-radius: 50%; background: radial-gradient(closest-side, rgba(0,0,0,.6), transparent); animation: tc7s 3.2s ease-in-out infinite; }
@keyframes tc7 { 50% { transform: translateY(-14px); } }
@keyframes tc7s { 50% { transform: scaleX(.75); opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .tc7 article, .tc7 .sh { animation: none; } }

About these CSS 3D Tilt Hover Cards

Perspective lives on the parent

3D rotation looks flat until something sets a viewing distance. Put perspective: 700px on the card's parent; smaller values exaggerate the effect, larger ones flatten it.

Keep tilt small

Ten to fifteen degrees reads as depth; more makes text hard to read and can cause motion sickness. Every demo checks prefers-reduced-motion and stays flat when it is set.

Depth inside the card

Give the card transform-style: preserve-3d and move children forward with translateZ(). When the card turns, nearer layers shift further, giving a parallax effect with no extra script.

Script only for following the pointer

A fixed tilt on hover needs only CSS. To follow the pointer, a few lines of JavaScript turn the pointer position into two angles and store them in custom properties that the CSS transform reads.

Frequently asked questions

Do tilt cards work on phones?

Phones have no hover, so the cards simply stay flat. That is the right default: the content is the same.

Why does text look blurry while tilted?

Rotated text is resampled. Keep angles small and the effect brief, and the card snaps back crisp when the pointer leaves.

Can I use a library instead?

You can, but the demos show the whole effect fits in a few lines of CSS and script.