Tilt Card with Glare for React
3D card with tilt and holographic glare effect that follows the cursor.
Component
Holographic Card
"use client";
import {
motion,
useMotionValue,
useTransform,
} from "framer-motion";
import { useCallback, useRef } from "react";
import { cn } from "@/lib/utils";
interface TiltCardGlareProps {
children: React.ReactNode;
className?: string;
glareClassName?: string;
glareColor?: string;
maxTilt?: number;
perspective?: number;
glareOpacity?: number;
glareIntensity?: number;
}
export function TiltCardGlare({
children,
className,
glareClassName,
glareColor = "rgba(255,255,255,0.8)",
maxTilt = 12,
perspective = 800,
glareOpacity = 0.4,
glareIntensity = 50,
}: TiltCardGlareProps) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(
y,
[-0.5, 0.5],
[maxTilt, -maxTilt],
);
const rotateY = useTransform(
x,
[-0.5, 0.5],
[-maxTilt, maxTilt],
);
const glareX = useTransform(
x,
[-0.5, 0.5],
[-glareIntensity, glareIntensity],
);
const glareY = useTransform(
y,
[-0.5, 0.5],
[-glareIntensity, glareIntensity],
);
const handleMouse = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const rect = ref.current?.getBoundingClientRect();
if (!rect) {
return;
}
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
x.set((e.clientX - cx) / (rect.width / 2));
y.set((e.clientY - cy) / (rect.height / 2));
},
[x, y],
);
const handleMouseLeave = useCallback(() => {
x.set(0);
y.set(0);
}, [x, y]);
return (
<motion.div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
style={{
rotateX,
rotateY,
transformPerspective: perspective,
}}
className={cn("relative overflow-hidden", className)}
>
<motion.div
className={cn(
"pointer-events-none absolute inset-0",
glareClassName,
)}
style={{
background: `radial-gradient(circle at 50% 50%, ${glareColor} 0%, transparent 50%)`,
opacity: glareOpacity,
x: glareX,
y: glareY,
}}
/>
{children}
</motion.div>
);
}Installation
1. Install dependencies
pnpm add framer-motion2. Copy the component file
"use client";
import {
motion,
useMotionValue,
useTransform,
} from "framer-motion";
import { useCallback, useRef } from "react";
import { cn } from "@/lib/utils";
interface TiltCardGlareProps {
children: React.ReactNode;
className?: string;
glareClassName?: string;
glareColor?: string;
maxTilt?: number;
perspective?: number;
glareOpacity?: number;
glareIntensity?: number;
}
export function TiltCardGlare({
children,
className,
glareClassName,
glareColor = "rgba(255,255,255,0.8)",
maxTilt = 12,
perspective = 800,
glareOpacity = 0.4,
glareIntensity = 50,
}: TiltCardGlareProps) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(
y,
[-0.5, 0.5],
[maxTilt, -maxTilt],
);
const rotateY = useTransform(
x,
[-0.5, 0.5],
[-maxTilt, maxTilt],
);
const glareX = useTransform(
x,
[-0.5, 0.5],
[-glareIntensity, glareIntensity],
);
const glareY = useTransform(
y,
[-0.5, 0.5],
[-glareIntensity, glareIntensity],
);
const handleMouse = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const rect = ref.current?.getBoundingClientRect();
if (!rect) {
return;
}
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
x.set((e.clientX - cx) / (rect.width / 2));
y.set((e.clientY - cy) / (rect.height / 2));
},
[x, y],
);
const handleMouseLeave = useCallback(() => {
x.set(0);
y.set(0);
}, [x, y]);
return (
<motion.div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
style={{
rotateX,
rotateY,
transformPerspective: perspective,
}}
className={cn("relative overflow-hidden", className)}
>
<motion.div
className={cn(
"pointer-events-none absolute inset-0",
glareClassName,
)}
style={{
background: `radial-gradient(circle at 50% 50%, ${glareColor} 0%, transparent 50%)`,
opacity: glareOpacity,
x: glareX,
y: glareY,
}}
/>
{children}
</motion.div>
);
}3. Import and use
import { TiltCardGlare } from "@/components/tilt-card-glare";
<TiltCardGlare className="rounded-2xl p-6">
Your content
</TiltCardGlare>;Usage
Import
Add the TiltCardGlare import.
import { TiltCardGlare } from "@/components/tilt-card-glare";Use
Wrap your card content.
<TiltCardGlare>Your content</TiltCardGlare>;Guidelines
- Wrap any card-like content. The component handles mouse tracking and transforms.
- maxTilt: rotation in degrees (default 12).
- perspective: CSS transform perspective (default 800).
- glareOpacity: 0–1. glareIntensity: how far the glare moves with cursor.
Props
All props are optional unless marked required. Use these to customize every aspect of the component.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | - | Card content (e.g. div, Card component). |
| className | string | - | Additional CSS classes for the wrapper. |
| glareClassName | string | - | Classes for the glare overlay. |
| glareColor | string | "rgba(255,255,255,0.8)" | Glare center color (CSS color, e.g. rgba(255,255,255,0.8) or amber tint). |
| maxTilt | number | 12 | Maximum tilt angle in degrees. |
| perspective | number | 800 | CSS transform perspective for 3D depth. |
| glareOpacity | number | 0.4 | Glare overlay opacity (0–1). |
| glareIntensity | number | 50 | How far the glare moves with cursor (pixels). |
Accessibility
- Decorative wrapper with no interactive controls, roles, or ARIA attributes of its own; any focusable elements you place as children keep their normal semantics and the wrapper does not interfere with them.
- The tilt and glare are driven only by onMouseMove and onMouseLeave, so the effect is mouse-only and has no keyboard, focus, or touch equivalent; the visual is purely cosmetic and not required to operate the content.
- It does not check prefers-reduced-motion, so the tilt and glare animate for everyone; it should be guarded with a matchMedia('(prefers-reduced-motion: reduce)') check that disables the motion for users who prefer reduced motion.
- Provide your own focus-visible styling and labels on any interactive children, since the wrapper adds none.
Performance
- Only transform-based properties animate: rotateX and rotateY on the card and an x/y translate plus opacity on the glare overlay, all of which the compositor can handle without triggering layout or paint.
- Motion is driven by framer-motion useMotionValue and useTransform updating in response to mouse events rather than a continuously running requestAnimationFrame loop, so nothing animates while the pointer is idle or off the card.
- Each mousemove calls getBoundingClientRect to map cursor position, which reads layout synchronously; it is lightweight here but fires on every pointer move.
- Listeners are attached via React onMouseMove and onMouseLeave props so they are cleaned up automatically on unmount; there are no manual addEventListener calls, timers, or IntersectionObservers to leak, and no will-change hint is set.
Examples
Basic
Wrap your card content for 3D tilt and glare.
Holographic Card
import { TiltCardGlare } from "@/components/tilt-card-glare";
<TiltCardGlare className="h-48 w-72 rounded-2xl bg-linear-to-br from-indigo-600 to-pink-500">
<div className="flex h-full items-center justify-center p-6">
<span className="font-bold text-white">
Holographic Card
</span>
</div>
</TiltCardGlare>;Custom tilt
Increase maxTilt for more dramatic effect.
Holographic Card
import { TiltCardGlare } from "@/components/tilt-card-glare";
<TiltCardGlare
maxTilt={20}
className="rounded-xl border p-6"
>
<p>Card with stronger tilt</p>
</TiltCardGlare>;Glare customization
Adjust perspective, glare opacity and travel distance.
Holographic Card
import { TiltCardGlare } from "@/components/tilt-card-glare";
<TiltCardGlare
perspective={1200}
glareOpacity={0.6}
glareIntensity={80}
glareClassName="rounded-2xl"
>
<div className="p-6">Subtle 3D</div>
</TiltCardGlare>;Custom gradient & glare
Card gradient via className; glare color via glareColor.
Holographic Card
import { TiltCardGlare } from "@/components/tilt-card-glare";
<TiltCardGlare
className="h-48 rounded-2xl bg-linear-to-br from-amber-500 to-orange-600"
glareColor="rgba(255,255,255,0.5)"
>
<div className="flex h-full items-center justify-center p-6 text-white">
Warm card
</div>
</TiltCardGlare>;Related reading
- Moving Things Without Moving Them — The 3D transforms behind the tilt
- Offloading Motion to the GPU — Keeping the tilt and glare on the GPU
Last updated on Jun 19