Gesture·09.6
Tilt Card
The card leans away from the cursor, lit from where you point.
AZAMAN
John Doe
5367 4567 8901 2345
Exp.
12/25
Install
No dependencies. The component is copied into your project, so the file is yours after that.
Or let the shadcn CLI do the copying: same file, landing in components/yugo.
bunx shadcn@latest add https://yugo.click/r/tilt-card.jsonUsage
"use client";
import { TiltCard } from "@/components/yugo/tilt-card";
export function Card() {
return (
<TiltCard tiltLimit={15} scale={1.05} perspective={1200}>
<div className="h-48 w-80 rounded-2xl bg-neutral-100 p-6 dark:bg-neutral-900">
Your content here
</div>
</TiltCard>
);
}Source
"use client"
import { useRef, useState, useCallback } from "react"
export interface TiltCardProps {
/** Maximum tilt angle in degrees */
tiltLimit?: number
/** Scale factor on hover */
scale?: number
/** Perspective distance in pixels */
perspective?: number
/** Tilt direction: "gravitate" follows cursor, "evade" tilts away */
effect?: "gravitate" | "evade"
/** Show a spotlight that follows the cursor on hover */
spotlight?: boolean
/** Additional class name */
className?: string
/** Additional inline styles */
style?: React.CSSProperties
/** Card content */
children?: React.ReactNode
}
export function TiltCard({
tiltLimit = 15,
scale = 1.05,
perspective = 1200,
effect = "evade",
spotlight = true,
className = "",
style,
children,
}: TiltCardProps) {
const cardRef = useRef<HTMLDivElement>(null)
const [transform, setTransform] = useState(
`perspective(${perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`
)
const [spotlightPos, setSpotlightPos] = useState({ x: 50, y: 50 })
const [isHovered, setIsHovered] = useState(false)
const dir = effect === "evade" ? -1 : 1
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
const el = cardRef.current
if (!el) return
const rect = el.getBoundingClientRect()
const px = (e.clientX - rect.left) / rect.width
const py = (e.clientY - rect.top) / rect.height
const xRot = (py - 0.5) * (tiltLimit * 2) * dir
const yRot = (px - 0.5) * -(tiltLimit * 2) * dir
setTransform(
`perspective(${perspective}px) rotateX(${xRot}deg) rotateY(${yRot}deg) scale3d(${scale}, ${scale}, ${scale})`
)
if (spotlight) {
setSpotlightPos({ x: px * 100, y: py * 100 })
}
},
[tiltLimit, scale, perspective, dir, spotlight]
)
const handlePointerEnter = useCallback(() => {
setIsHovered(true)
}, [])
const handlePointerLeave = useCallback(() => {
setTransform(
`perspective(${perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`
)
setIsHovered(false)
}, [perspective])
return (
<div
ref={cardRef}
onPointerEnter={handlePointerEnter}
onPointerMove={handlePointerMove}
onPointerLeave={handlePointerLeave}
className={`will-change-transform relative overflow-hidden ${className}`}
style={{
transform,
transition: "transform 0.2s ease-out",
transformStyle: "preserve-3d",
...style,
}}
>
{children}
{spotlight && (
<div
className="pointer-events-none absolute inset-0 z-10 overflow-hidden"
style={{ opacity: isHovered ? 1 : 0, transition: "opacity 0.3s" }}
>
<div
className="absolute w-[200%] h-[200%] rounded-full opacity-100 dark:opacity-50"
style={{
left: `${spotlightPos.x}%`,
top: `${spotlightPos.y}%`,
transform: "translate(-50%, -50%)",
background:
"radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 40%)",
}}
/>
</div>
)}
</div>
)
}Props
tiltLimit15numberMaximum tilt angle in degrees.
scale1.05numberScale factor on hover.
perspective1200numberPerspective distance in pixels.
effect"evade""gravitate" | "evade"Tilt direction: follows cursor or tilts away.
spotlighttruebooleanShow a spotlight that follows the cursor on hover.
childrenReact.ReactNodeCard content.
classNamestringAdditional CSS classes.
styleReact.CSSPropertiesAdditional inline styles.