← All primitives
TiltCard
InteractionPointer-driven 3D tilt with a moving specular glare. Pure CSS transforms via CSS variables, no per-frame React state.
Requires the .prism-tilt styles from globals.css.
$
View raw manifest →npx shadcn@latest add https://prism.icglabs.co/r/tilt-card.jsoncomponents/prism/TiltCard.tsx
"use client";
import { useRef } from "react";
/**
* TiltCard — a 3D tilt that follows the pointer, with a moving specular glare.
* Pure CSS transforms driven by CSS variables (no per-frame React state). Styles
* in globals.css. Honors prefers-reduced-motion (stays flat).
*/
export function TiltCard({
children,
className = "",
max = 12,
}: {
children: React.ReactNode;
className?: string;
/** max tilt in degrees */
max?: number;
}) {
const ref = useRef<HTMLDivElement>(null);
function onMove(e: React.MouseEvent<HTMLDivElement>) {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const r = el.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width - 0.5;
const py = (e.clientY - r.top) / r.height - 0.5;
el.style.setProperty("--tilt-rx", `${(-py * max).toFixed(2)}deg`);
el.style.setProperty("--tilt-ry", `${(px * max).toFixed(2)}deg`);
el.style.setProperty("--tilt-gx", `${(px * 100 + 50).toFixed(1)}%`);
el.style.setProperty("--tilt-gy", `${(py * 100 + 50).toFixed(1)}%`);
}
function onLeave() {
const el = ref.current;
if (!el) return;
el.style.setProperty("--tilt-rx", "0deg");
el.style.setProperty("--tilt-ry", "0deg");
}
return (
<div ref={ref} onMouseMove={onMove} onMouseLeave={onLeave} className={`prism-tilt ${className}`}>
{children}
<span aria-hidden className="prism-tilt-glare" />
</div>
);
}