Card Tilt Hover Effect
Original · freeA card that tilts in 3D toward the cursor on hover.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/hover-card-tilt.json"use client";
import { type PointerEvent, useCallback, useRef, useState } from "react";
type Tilt = { rx: number; ry: number; gx: number; gy: number };
const REST: Tilt = { rx: 0, ry: 0, gx: 50, gy: 50 };
const MAX_DEG = 12;
export default function HoverCardTilt() {
const cardRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null);
const [tilt, setTilt] = useState<Tilt>(REST);
const [active, setActive] = useState(false);
const reduced =
typeof window !== "undefined" &&
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
const compute = useCallback(
(clientX: number, clientY: number) => {
const el = cardRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const px = (clientX - rect.left) / rect.width;
const py = (clientY - rect.top) / rect.height;
const clampedX = Math.min(Math.max(px, 0), 1);
const clampedY = Math.min(Math.max(py, 0), 1);
setTilt({
rx: (0.5 - clampedY) * MAX_DEG * 2,
ry: (clampedX - 0.5) * MAX_DEG * 2,
gx: clampedX * 100,
gy: clampedY * 100,
});
},
[],
);
const onPointerMove = useCallback(
(e: PointerEvent<HTMLDivElement>) => {
if (reduced) return;
const { clientX, clientY } = e;
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => compute(clientX, clientY));
},
[compute, reduced],
);
const reset = useCallback(() => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
setActive(false);
setTilt(REST);
}, []);
const engage = useCallback(() => {
if (!reduced) setActive(true);
}, [reduced]);
return (
<section className="relative w-full overflow-hidden bg-slate-100 px-6 py-20 dark:bg-slate-950 sm:py-28">
<style>{`
@keyframes hctilt_float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes hctilt_shine {
from { transform: translateX(-120%) skewX(-18deg); }
to { transform: translateX(320%) skewX(-18deg); }
}
@media (prefers-reduced-motion: reduce) {
.hctilt_badge { animation: none !important; }
.hctilt_shine { display: none !important; }
}
`}</style>
{/* atmosphere */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-[radial-gradient(60%_50%_at_50%_0%,rgba(99,102,241,0.18),transparent_70%)] dark:bg-[radial-gradient(60%_50%_at_50%_0%,rgba(129,140,248,0.16),transparent_70%)]"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-[0.4] [background-image:linear-gradient(to_right,rgba(100,116,139,0.12)_1px,transparent_1px),linear-gradient(to_bottom,rgba(100,116,139,0.12)_1px,transparent_1px)] [background-size:44px_44px] [mask-image:radial-gradient(70%_60%_at_50%_40%,black,transparent)]"
/>
<div className="relative mx-auto flex max-w-md flex-col items-center">
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.25em] text-indigo-600 dark:text-indigo-400">
Hover to interact
</p>
<h2 className="mb-10 text-center text-2xl font-semibold text-slate-900 dark:text-slate-100">
Move your cursor across the card
</h2>
<div
className="group"
style={{ perspective: "1100px" }}
onPointerMove={onPointerMove}
onPointerEnter={engage}
onPointerLeave={reset}
>
<div
ref={cardRef}
role="button"
tabIndex={0}
aria-label="Aurora Pro membership card — interactive 3D tilt on hover"
onFocus={engage}
onBlur={reset}
className="relative w-[min(88vw,22rem)] rounded-3xl border border-white/60 bg-white/80 p-7 shadow-[0_18px_50px_-18px_rgba(15,23,42,0.45)] outline-none ring-indigo-500/60 backdrop-blur-xl transition-[transform,box-shadow] duration-200 ease-out will-change-transform focus-visible:ring-2 dark:border-white/10 dark:bg-slate-900/70 dark:shadow-[0_18px_60px_-18px_rgba(0,0,0,0.8)]"
style={{
transform: `rotateX(${tilt.rx}deg) rotateY(${tilt.ry}deg) scale(${
active ? 1.03 : 1
})`,
transformStyle: "preserve-3d",
}}
>
{/* dynamic glare that follows the cursor */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 rounded-3xl opacity-0 transition-opacity duration-200 group-hover:opacity-100"
style={{
background: `radial-gradient(circle at ${tilt.gx}% ${tilt.gy}%, rgba(255,255,255,0.55), rgba(255,255,255,0) 42%)`,
mixBlendMode: "overlay",
}}
/>
{/* header row — lifted in Z */}
<div
className="relative flex items-center justify-between"
style={{ transform: "translateZ(48px)" }}
>
<span className="hctilt_badge inline-flex items-center gap-2 rounded-full bg-indigo-600/10 px-3 py-1 text-xs font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-600/20 dark:bg-indigo-400/10 dark:text-indigo-300 dark:ring-indigo-400/20 [animation:hctilt_float_4s_ease-in-out_infinite]">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Active
</span>
<svg
viewBox="0 0 24 24"
className="h-7 w-7 text-slate-900 dark:text-slate-100"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 3l2.4 5.4L20 9.3l-4 4 1 5.7-5-2.9-5 2.9 1-5.7-4-4 5.6-.9z" />
</svg>
</div>
{/* title block — deepest Z lift */}
<div
className="relative mt-8"
style={{ transform: "translateZ(64px)" }}
>
<h3 className="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
Aurora Pro
</h3>
<p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
Unlimited projects, priority render queue, and early access to
every experimental brush pack we ship.
</p>
</div>
{/* price + chip — mid Z lift */}
<div
className="relative mt-7 flex items-end justify-between"
style={{ transform: "translateZ(40px)" }}
>
<div>
<span className="text-3xl font-bold text-slate-900 dark:text-white">
$18
</span>
<span className="ml-1 text-sm text-slate-500 dark:text-slate-400">
/month
</span>
</div>
<div className="relative h-8 w-11 overflow-hidden rounded-md bg-gradient-to-br from-amber-300 to-amber-500 shadow-inner">
<span className="absolute left-1 top-1 h-3 w-4 rounded-[3px] bg-amber-200/70" />
<span className="hctilt_shine absolute inset-y-0 left-0 w-1/3 bg-white/50 [animation:hctilt_shine_2.6s_ease-in-out_infinite]" />
</div>
</div>
{/* CTA — top Z lift */}
<button
type="button"
className="relative mt-8 w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white shadow-lg transition-colors hover:bg-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
style={{ transform: "translateZ(56px)" }}
>
Upgrade now
</button>
</div>
</div>
<p className="mt-8 text-center text-xs text-slate-500 dark:text-slate-500">
Tilt follows your pointer. Keyboard focus and reduced-motion are
respected.
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Card Lift Hover Effect
OriginalA card that lifts with a growing shadow on hover.

Card Glow Hover Effect
OriginalA card with a coloured glow that blooms on hover.

Card Border Draw Hover Effect
OriginalA card whose border draws itself in on hover.

Card Gradient Shift Hover Effect
OriginalA card whose gradient background shifts on hover.

Card Spotlight Hover Effect
OriginalA card with a cursor-following radial spotlight.

Link Underline Hover Effect
OriginalAnimated link underline effects: grow, slide and centre-out.

Link Slide Hover Effect
OriginalLinks whose label slides up to a duplicate on hover.

Button Fill Hover Effect
OriginalButtons with a colour fill that sweeps across on hover.

Button Shine Hover Effect
OriginalButtons with a diagonal shine sweep on hover.
Icon Fill Hover Effect
OriginalIcon buttons whose icon and background fill on hover.
Icon Bounce Hover Effect
OriginalIcons that bounce or spin playfully on hover.

List Highlight Hover Effect
OriginalA list where the hovered row gets a sliding highlight and arrow.

