Prism
← All primitives

TiltCard

Interaction

Pointer-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.

$npx shadcn@latest add https://prism.icglabs.co/r/tilt-card.json
View raw manifest →

components/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>
  );
}
Live demo — read-only. Every section is a real, copyable primitive.