Holographic Card
A foil sheen of shifting colour follows the pointer across the card, like tilting a collectible under a lamp.
Rare edition
No. 042 / 300
Create a holographic foil card in React and CSS. On pointermove, write the pointer position as 0–1 fractions into CSS custom properties --x and --y on the card, and set --on to 1; on pointerleave, set --on back to 0. Add an ::before overlay with a repeating-linear-gradient of soft rainbow hues at 115deg, background-size 200% 200%, and background-position calc(var(--x) * 100%) calc(var(--y) * 100%), blended with mix-blend-mode: color-dodge at opacity calc(var(--on) * 0.3). Add an ::after with a radial-gradient white glare centred at the same position. Tilt the card at most 6deg with rotateY(calc((var(--x) - 0.5) * 12deg * var(--on))) and the matching rotateX, with a 120ms transition so it follows smoothly. Under prefers-reduced-motion, keep the sheen but drop the tilt. The card reads "Rare edition" and "No. 042 / 300".
Source
'use client'
import s from './demo.module.css'
export default function HolographicCard({ labels }: { labels: Record<string, string> }) {
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
const el = e.currentTarget
const r = el.getBoundingClientRect()
el.style.setProperty('--x', String((e.clientX - r.left) / r.width))
el.style.setProperty('--y', String((e.clientY - r.top) / r.height))
el.style.setProperty('--on', '1')
}
const onLeave = (e: React.PointerEvent<HTMLDivElement>) => e.currentTarget.style.setProperty('--on', '0')
return (
<div className={s.card} onPointerMove={onMove} onPointerLeave={onLeave}>
<span className={s.badge}>★</span>
<div>
<p className={s.title}>{labels.title}</p>
<p className={s.sub}>{labels.sub}</p>
</div>
</div>
)
}
.card {
--x: 0.5;
--y: 0.5;
--on: 0;
position: relative;
display: grid;
align-content: space-between;
width: 136px;
aspect-ratio: 5 / 7;
padding: 14px;
overflow: hidden;
border: 1px solid var(--stage-rule);
border-radius: 14px;
background: linear-gradient(160deg, var(--stage-surface), var(--stage-bg));
transform: perspective(600px) rotateY(calc((var(--x) - 0.5) * 12deg * var(--on)))
rotateX(calc((0.5 - var(--y)) * 12deg * var(--on)));
transition: transform 120ms linear;
}
.card::before,
.card::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
transition: opacity var(--dur-long) var(--ease-out);
}
.card::before {
background: repeating-linear-gradient(
115deg,
oklch(80% 0.08 20),
oklch(85% 0.08 90),
oklch(82% 0.08 160),
oklch(78% 0.08 250),
oklch(80% 0.08 320),
oklch(80% 0.08 20) 40%
);
background-size: 200% 200%;
background-position: calc(var(--x) * 100%) calc(var(--y) * 100%);
mix-blend-mode: color-dodge;
opacity: calc(var(--on) * 0.3);
}
.card::after {
background: radial-gradient(circle at calc(var(--x) * 100%) calc(var(--y) * 100%), var(--stage-sheen), transparent 45%);
opacity: var(--on);
}
.badge {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--stage-accent);
color: var(--stage-accent-ink);
font-size: var(--text-sm);
}
.title {
margin: 0;
font: 600 var(--text-lg) / 1.2 var(--font-display);
color: var(--stage-ink);
}
.sub {
margin: 4px 0 0;
font: 500 var(--text-xs) / 1 var(--font-mono);
color: var(--stage-muted);
}
@media (prefers-reduced-motion: reduce) {
.card { transform: none; }
}