Components / Tilt Card
Tilt Card
A lightweight 3D tilt effect that responds to mouse movement. Wrap any card or image to add subtle depth and interactivity.
Preview
Move your cursor over the card to see the tilt effect.

Usage
tsx
import TiltCard from "@/components/tilt-image/tilt-image"
export function Example() {
return (
<TiltCard maxRotation={4} perspective={400} scaleOnHover={1.04}>
<figure>
<img
src="/images/photo.jpg"
alt="Example"
className="rounded-xl shadow-lg w-[280px] h-[360px] object-cover"
/>
</figure>
</TiltCard>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | required | Content to tilt (usually an image or card) |
| maxRotation | number | 3 | Maximum rotation in degrees on X/Y axes |
| perspective | number | 300 | 3D perspective distance in pixels |
| scaleOnHover | number | 1.02 | Scale factor applied while hovering |
Notes
- Effect is mouse-based and will not animate on touch devices.
- Wrap only one element (like a card or figure) for best results.
- Use smaller
maxRotationfor subtle motion. - No external animation libraries required.
Source Code
tsx
'use client'
import { useRef, useEffect, useCallback, ReactNode } from 'react'
export interface TiltCardProps {
children: ReactNode
maxRotation?: number
perspective?: number
scaleOnHover?: number
}
const TiltCard = ({
children,
maxRotation = 3,
perspective = 300,
scaleOnHover = 1.02
}: TiltCardProps) => {
const cardRef = useRef<HTMLDivElement>(null)
const rafId = useRef<number | null>(null)
const dimensions = useRef({ width: 0, height: 0 })
const updateDimensions = useCallback(() => {
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect()
dimensions.current = {
width: rect.width,
height: rect.height
}
}
}, [])
useEffect(() => {
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => {
window.removeEventListener('resize', updateDimensions)
if (rafId.current) cancelAnimationFrame(rafId.current)
}
}, [updateDimensions])
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!cardRef.current) return
if (rafId.current) cancelAnimationFrame(rafId.current)
rafId.current = requestAnimationFrame(() => {
if (!cardRef.current) return
const { width, height } = dimensions.current
const centerX = width / 2
const centerY = height / 2
const rect = cardRef.current.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const rotateY = ((x - centerX) / centerX) * maxRotation
const rotateX = -((y - centerY) / centerY) * maxRotation
cardRef.current.style.transform = `
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale3d(${scaleOnHover}, ${scaleOnHover}, ${scaleOnHover})
`
})
},
[maxRotation, scaleOnHover]
)
const handleMouseLeave = useCallback(() => {
if (rafId.current) cancelAnimationFrame(rafId.current)
if (cardRef.current) {
cardRef.current.style.transform = `
rotateX(0deg)
rotateY(0deg)
scale3d(1, 1, 1)
`
}
}, [])
return (
<div style={{ perspective: `${perspective}px` }} className="">
<div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="transition-transform duration-300 ease-out"
>
{children}
</div>
</div>
)
}
export default TiltCard