Component
Holographic Card
A trading-card-style hover effect: the card tilts toward your cursor in 3D, a rainbow foil sheen sweeps across it, and a soft glare follows your pointer — the same "holo" feel as a physical foil card, built entirely with CSS gradients and spring physics. Wrap any card content in it; no extra markup required.
Features
- ▸Feels physical : The card tilts in 3D toward the cursor with spring physics, like you're turning a real card in your hand.
- ▸Magic on Hover : A rainbow foil sheen and glare sweep across the surface, rewarding visitors who linger instead of scrolling past.
- ▸Never feels gimmicky twice : the tilt and shine track the cursor precisely, so it always feels responsive rather than looping on a timer.
- ▸Wraps any content : a pricing tier, a team member, a product tile — the effect is the wrapper, not a fixed card design.
- ▸Responsive : Since tilt has no meaning without a cursor, the card stays flat and calm on touch devices instead of flashing an odd half-effect.
Legendary
Spark Core
Power∞ / 100
Epic
Motion Rune
Power94 / 100
Rare
Glass Shard
Power87 / 100
Installation
01.Install Dependencies
npm install motion clsx tailwind-mergeSource Code
"use client";
import * as React from "react";
import {
motion,
useMotionTemplate,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import { cn } from "@/utils/cn";
const SPRING = { stiffness: 220, damping: 22, mass: 0.6 };
export function HolographicCard({
children,
className,
intensity = 12,
foilOpacity = 0.5,
}) {
const px = useMotionValue(0.5);
const py = useMotionValue(0.5);
const [hovering, setHovering] = React.useState(false);
const [enabled, setEnabled] = React.useState(true);
React.useEffect(() => {
setEnabled(!window.matchMedia("(pointer: coarse)").matches);
}, []);
const rotateX = useSpring(useTransform(py, [0, 1], [intensity, -intensity]), SPRING);
const rotateY = useSpring(useTransform(px, [0, 1], [-intensity, intensity]), SPRING);
const glareX = useSpring(useTransform(px, [0, 1], [0, 100]), SPRING);
const glareY = useSpring(useTransform(py, [0, 1], [0, 100]), SPRING);
const foilShift = useSpring(
useTransform([px, py], (v) => {
const [x, y] = v;
return (x + y) * 50;
}),
SPRING
);
const glare = useMotionTemplate`radial-gradient(circle at ${glareX}% ${glareY}%, rgba(255,255,255,0.45), transparent 45%)`;
const foilPosition = useMotionTemplate`${foilShift}% 50%`;
const handlePointerMove = (e) => {
if (!enabled) return;
const rect = e.currentTarget.getBoundingClientRect();
px.set((e.clientX - rect.left) / rect.width);
py.set((e.clientY - rect.top) / rect.height);
};
const handlePointerEnter = () => {
if (enabled) setHovering(true);
};
const handlePointerLeave = () => {
px.set(0.5);
py.set(0.5);
setHovering(false);
};
return (
<div style={{ perspective: 1000 }}>
<motion.div
onPointerMove={handlePointerMove}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
className={cn(
"group relative overflow-hidden rounded-2xl border border-white/10 bg-zinc-900",
className
)}
>
{children}
<motion.div
aria-hidden
animate={{ opacity: hovering ? foilOpacity : 0 }}
transition={{ duration: 0.4 }}
style={{
backgroundImage:
"linear-gradient(115deg, transparent 20%, rgba(255,0,132,0.7) 36%, rgba(255,214,0,0.7) 43%, rgba(0,255,163,0.7) 50%, rgba(0,178,255,0.7) 57%, rgba(172,0,255,0.7) 64%, transparent 80%)",
backgroundSize: "300% 300%",
backgroundPosition: foilPosition,
mixBlendMode: "color-dodge",
}}
className="pointer-events-none absolute inset-0"
/>
<motion.div
aria-hidden
animate={{ opacity: hovering ? 0.25 : 0 }}
transition={{ duration: 0.4 }}
style={{
backgroundImage:
"repeating-linear-gradient(0deg, rgba(255,255,255,0.12) 0px, rgba(255,255,255,0.12) 1px, transparent 1px, transparent 3px)",
mixBlendMode: "overlay",
}}
className="pointer-events-none absolute inset-0"
/>
<motion.div
aria-hidden
animate={{ opacity: hovering ? 1 : 0 }}
transition={{ duration: 0.4 }}
style={{ backgroundImage: glare, mixBlendMode: "overlay" }}
className="pointer-events-none absolute inset-0"
/>
</motion.div>
</div>
);
}