Tilt Card
A bounded spring tilt with stable pointer tracking and reduced-motion support.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/tilt-card.jsonUse it
Scroll horizontally for long lines
"use client";
import {TiltCard} from '@/components/jez-ui/ui/tilt-card';
export default function Example(){
return <><TiltCard className="w-full max-w-sm border-0 bg-primary p-0 text-primary-foreground"><div className="flex min-h-80 flex-col justify-between p-8"><div className="flex justify-between text-sm"><span>Jez Studio</span><span>↗</span></div><h3 className="text-5xl font-medium leading-[1.05] tracking-tight">A different<br/>perspective.</h3><div className="flex justify-between border-t border-white/30 pt-5 text-xs"><span>Move your pointer.</span><span>Make it yours.</span></div></div></TiltCard></>;
}Props & types
Scroll horizontally for long lines
{
children: React.ReactNode;
className?: string;
maxTilt?: number;
}Source
View 2 source files
registry/ui/tilt-card.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
import { motion, useReducedMotion, useSpring } from "motion/react";
export function TiltCard({
children,
className,
maxTilt = 8,
}: {
children: React.ReactNode;
className?: string;
maxTilt?: number;
}) {
const reduce = useReducedMotion();
const rotateX = useSpring(0, { stiffness: 210, damping: 25 });
const rotateY = useSpring(0, { stiffness: 210, damping: 25 });
const reset = () => {
rotateX.set(0);
rotateY.set(0);
};
React.useEffect(() => {
if (reduce) {
rotateX.jump(0);
rotateY.jump(0);
}
}, [reduce, rotateX, rotateY]);
return (
<div
style={{ perspective: 1000 }}
onPointerMove={(e) => {
if (reduce || e.pointerType !== "mouse") return;
// Measure the stationary wrapper, not the surface being transformed.
const r = e.currentTarget.getBoundingClientRect();
rotateX.set(
-Math.max(-1, Math.min(1, ((e.clientY - r.top) / r.height) * 2 - 1)) *
maxTilt,
);
rotateY.set(
Math.max(-1, Math.min(1, ((e.clientX - r.left) / r.width) * 2 - 1)) *
maxTilt,
);
}}
onPointerLeave={reset}
onPointerCancel={reset}
>
<motion.div
style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
className={cn(
"rounded-xl border border-border bg-background p-8",
className,
)}
>
{children}
</motion.div>
</div>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
Dependencies
motion@13.2.0 · clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Respect reduced-motion preferences. Decorative effects must not carry essential information. WebGL scenes include a static fallback and only render while visible.
Read the accessibility and performance guide →