Tilt Card
A 3D card that tilts toward the cursor with spring physics.
- Category
- Components
- Added
- 2026-08-10
- Deps
- 1
- Updated
- 2026-09-01
Editorial Card
Tilts toward your cursor with a soft spring.
Props
"use client";
import { useRef } from "react";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
export function TiltCard({
title = "Editorial Card",
body = "Tilts toward your cursor with a soft spring.",
maxTilt = 12,
glare = true,
}: {
title?: string;
body?: string;
maxTilt?: number;
/** Soft highlight across the upper-left face, as if lit from there. */
glare?: boolean;
}) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [maxTilt, -maxTilt]), {
stiffness: 200,
damping: 20,
});
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-maxTilt, maxTilt]), {
stiffness: 200,
damping: 20,
});
function handleMove(e: React.MouseEvent<HTMLDivElement>) {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
x.set((e.clientX - rect.left) / rect.width - 0.5);
y.set((e.clientY - rect.top) / rect.height - 0.5);
}
return (
<div style={{ perspective: 800 }}>
<motion.div
ref={ref}
onMouseMove={handleMove}
onMouseLeave={() => {
x.set(0);
y.set(0);
}}
style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
className="relative w-64 rounded-2xl border border-black/10 bg-white p-6 shadow-xl"
>
{glare && (
<div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-br from-white/40 via-transparent to-transparent" />
)}
<div className="mb-3 h-8 w-8 rounded-full bg-red-500" />
<h3 className="mb-1 text-lg font-bold">{title}</h3>
<p className="text-sm text-black/60">{body}</p>
</motion.div>
</div>
);
}
About this component
A hover-scale card is the default reach when a card needs to feel interactive, but it doesn't convey depth the way a real 3D tilt toward the cursor does. This tracks pointer position as two motion values, maps them through useTransform into rotation ranges, and runs the result through useSpring so the tilt settles with weight instead of snapping back. The detail that matters when adapting the markup: perspective has to live on a separate parent wrapper, not the same element that carries the 3D transform style and the rotation values — putting both on one element breaks the depth read entirely. Reach for this over a vanilla-tilt.js integration when Framer Motion is already in the project and a second, unrelated tilt dependency isn't worth adding for one card. One thing to note: the glare overlay visible in the live preview is preview-only, controlled by a prop that doesn't exist in the plain exported code, so copying just the code tab won't include it.
Curator’s note
Tuned to feel weighty, not floaty — lower stiffness reads cheap on this kind of card.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Magnetic Button
componentsA button whose label leans toward the cursor with a springy pull.
Liquid Blob Cursor Follow
backgroundsA soft blurred blob that trails the cursor with spring-smoothed lag.