Tilt Card
ui-beats/tilt-cardFreeComponent
The TiltCard component tilts toward the pointer in 3D and tracks it with a specular highlight, giving flat cards a tactile, physical feel.
Install it
npx shadcn@latest add https://uibeats.com/r/tilt-card.jsonSource
1"use client";23import { useRef, type ReactNode } from "react";4import {5 motion,6 useMotionTemplate,7 useMotionValue,8 useReducedMotion,9 useSpring,10} from "motion/react";1112interface TiltCardProps {13 children: ReactNode;14 /** Maximum rotation in degrees at the edges of the card. */15 maxTilt?: number;16 /** Scale applied while the pointer is over the card. */17 hoverScale?: number;18 /** Strength of the specular highlight, 0 to 1. */19 glareOpacity?: number;20 className?: string;21}2223/**24 * A card that tilts toward the pointer in 3D, with a specular highlight that25 * tracks the cursor.26 */27export function TiltCard({28 children,29 maxTilt = 12,30 hoverScale = 1.03,31 glareOpacity = 0.25,32 className = "",33}: TiltCardProps) {34 const ref = useRef<HTMLDivElement>(null);35 const prefersReducedMotion = useReducedMotion();3637 // Pointer position as CSS percentages, fed straight into the glare gradient.38 const px = useMotionValue("50%");39 const py = useMotionValue("50%");4041 const spring = { stiffness: 260, damping: 24, mass: 0.6 };42 const rotateX = useSpring(useMotionValue(0), spring);43 const rotateY = useSpring(useMotionValue(0), spring);44 const scale = useSpring(useMotionValue(1), spring);4546 /*47 * Visibility of the highlight, separate from its position.48 *49 * Without this the gradient was painted the whole time, parked at the50 * card's centre — so the card sat there with a permanent bright blob on it51 * whether or not a pointer was anywhere near. A highlight is a response to52 * the pointer, so it starts at zero and fades in on enter.53 */54 const glareStrength = useSpring(useMotionValue(0), {55 stiffness: 180,56 damping: 26,57 });5859 const glare = useMotionTemplate`radial-gradient(circle at ${px} ${py}, rgba(255,255,255,${glareOpacity}), transparent 60%)`;6061 const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {62 const element = ref.current;63 if (!element || prefersReducedMotion) return;6465 const rect = element.getBoundingClientRect();66 const x = (event.clientX - rect.left) / rect.width;67 const y = (event.clientY - rect.top) / rect.height;6869 px.set(`${x * 100}%`);70 py.set(`${y * 100}%`);7172 // Invert X so the card leans toward the cursor rather than away from it.73 rotateX.set(-(y - 0.5) * 2 * maxTilt);74 rotateY.set((x - 0.5) * 2 * maxTilt);75 };7677 const enter = () => {78 if (prefersReducedMotion) return;79 scale.set(hoverScale);80 glareStrength.set(1);81 };8283// … truncated
What it pulls in
npm packages
Files it writes
More from ui-beats
All 34 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Animated Beamanimated-beam | ui-beats | Components | Free | 1 dep | |
| Animated Listanimated-list | ui-beats | Components | Free | 1 dep | |
| Border Beamborder-beam | ui-beats | Components | Free | 1 dep | |
| Bouncebounce | ui-beats | Components | Free | 1 dep | |
| Card Stackcard-stack | ui-beats | Components | Free | 1 dep | |
| Dockdock | ui-beats | Components | Free | 1 dep | |
| Fade Infade-in | ui-beats | Components | Free | 1 dep | |
| Fade In Unblurfade-in-unblur | ui-beats | Components | Free | 1 dep |
