Tilt Card
3D perspective tilt with a moving glare highlight.
Kinetiq Pay
4821 •••• •••• 0921
Alex Morgan09/29
components/ui/tilt-card.tsx
"use client";
import { motion, useMotionTemplate, useMotionValue, useSpring, useTransform } from "motion/react";
import { Wifi } from "lucide-react";
import type { ReactNode } from "react";
type TiltCardProps = {
children: ReactNode;
className?: string;
/** Maximum rotation in degrees */
maxTilt?: number;
};
export function TiltCard({ children, className = "", maxTilt = 12 }: TiltCardProps) {
const px = useMotionValue(0.5);
const py = useMotionValue(0.5);
const rotateX = useSpring(useTransform(py, [0, 1], [maxTilt, -maxTilt]), { stiffness: 180, damping: 18 });
const rotateY = useSpring(useTransform(px, [0, 1], [-maxTilt, maxTilt]), { stiffness: 180, damping: 18 });
const glareX = useTransform(px, [0, 1], [0, 100]);
const glareY = useTransform(py, [0, 1], [0, 100]);
const glare = useMotionTemplate`radial-gradient(circle at ${glareX}% ${glareY}%, rgba(255,255,255,0.35), transparent 55%)`;
return (
<motion.div
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
px.set((e.clientX - rect.left) / rect.width);
py.set((e.clientY - rect.top) / rect.height);
}}
onMouseLeave={() => {
px.set(0.5);
py.set(0.5);
}}
style={{ rotateX, rotateY, transformPerspective: 900 }}
className={`relative overflow-hidden rounded-2xl ${className}`}
>
{children}
<motion.div aria-hidden className="pointer-events-none absolute inset-0" style={{ background: glare }} />
</motion.div>
);
}
export default function Demo() {
return (
<TiltCard className="aspect-[1.586] w-80 bg-gradient-to-br from-zinc-900 via-zinc-800 to-blue-900 p-6 text-white shadow-2xl shadow-zinc-900/30">
<div className="flex h-full flex-col justify-between">
<div className="flex items-start justify-between">
<span className="text-sm font-semibold tracking-wide">Kinetiq Pay</span>
<Wifi className="size-5 rotate-90 opacity-70" />
</div>
<div className="h-8 w-11 rounded-md bg-gradient-to-br from-amber-200 to-amber-400" />
<div>
<p className="font-mono text-lg tracking-[0.2em]">4821 •••• •••• 0921</p>
<div className="mt-2 flex justify-between text-xs uppercase tracking-wider text-white/60">
<span>Alex Morgan</span>
<span>09/29</span>
</div>
</div>
</div>
</TiltCard>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/tilt-card.tsxcomponents/ui/tilt-card.tsx"use client"; import { motion, useMotionTemplate, useMotionValue, useSpring, useTransform } from "motion/react"; import { Wifi } from "lucide-react"; import type { ReactNode } from "react"; type TiltCardProps = { children: ReactNode; className?: string; /** Maximum rotation in degrees */ maxTilt?: number; }; export function TiltCard({ children, className = "", maxTilt = 12 }: TiltCardProps) { const px = useMotionValue(0.5); const py = useMotionValue(0.5); const rotateX = useSpring(useTransform(py, [0, 1], [maxTilt, -maxTilt]), { stiffness: 180, damping: 18 }); const rotateY = useSpring(useTransform(px, [0, 1], [-maxTilt, maxTilt]), { stiffness: 180, damping: 18 }); const glareX = useTransform(px, [0, 1], [0, 100]); const glareY = useTransform(py, [0, 1], [0, 100]); const glare = useMotionTemplate`radial-gradient(circle at ${glareX}% ${glareY}%, rgba(255,255,255,0.35), transparent 55%)`; return ( <motion.div onMouseMove={(e) => { const rect = e.currentTarget.getBoundingClientRect(); px.set((e.clientX - rect.left) / rect.width); py.set((e.clientY - rect.top) / rect.height); }} onMouseLeave={() => { px.set(0.5); py.set(0.5); }} style={{ rotateX, rotateY, transformPerspective: 900 }} className={`relative overflow-hidden rounded-2xl ${className}`} > {children} <motion.div aria-hidden className="pointer-events-none absolute inset-0" style={{ background: glare }} /> </motion.div> ); } export default function Demo() { return ( <TiltCard className="aspect-[1.586] w-80 bg-gradient-to-br from-zinc-900 via-zinc-800 to-blue-900 p-6 text-white shadow-2xl shadow-zinc-900/30"> <div className="flex h-full flex-col justify-between"> <div className="flex items-start justify-between"> <span className="text-sm font-semibold tracking-wide">Kinetiq Pay</span> <Wifi className="size-5 rotate-90 opacity-70" /> </div> <div className="h-8 w-11 rounded-md bg-gradient-to-br from-amber-200 to-amber-400" /> <div> <p className="font-mono text-lg tracking-[0.2em]">4821 •••• •••• 0921</p> <div className="mt-2 flex justify-between text-xs uppercase tracking-wider text-white/60"> <span>Alex Morgan</span> <span>09/29</span> </div> </div> </div> </TiltCard> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.