Cards
Holographic Card
A foil-finished card that tilts in 3D while its spectral pattern slides against your cursor, so the sheen reads as depth.
Original by Plinth · MIT
Report a problem or suggest an improvementInstall
npx shadcn@latest add https://plinthui.com/r/holographic-card.json'use client';
import { useCallback, useRef } from 'react';
// The pointer position lives in CSS custom properties rather than React state.
// Re-rendering on every pointermove would be a render per frame for something
// that changes no markup — writing two properties on the node is the whole
// update, and the tilt, foil and glare all read them.
const css = `
.holo {
/* Pointer position as a 0..1 pair, written by script and read by every layer
below. Keeping it in custom properties means one listener drives the tilt,
the foil and the glare together — they can never disagree about where the
cursor is, which is what separate listeners always eventually do. */
--hx: 0.5;
--hy: 0.5;
position: relative;
width: 280px;
aspect-ratio: 5 / 7;
border-radius: 18px;
overflow: hidden;
isolation: isolate;
background:
radial-gradient(120% 90% at 30% 0%, #2a1b53 0%, #140d2b 55%, #0b0718 100%);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.08);
/* Perspective on the element itself, so the tilt is a real 3D rotation
rather than a 2D skew pretending to be one. */
transform: perspective(900px)
rotateY(calc((var(--hx) - 0.5) * 18deg))
rotateX(calc((0.5 - var(--hy)) * 18deg));
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
will-change: transform;
}
/* The foil. Two gradients at different angles multiplied together give the
uneven, patchy interference of real holographic stock — a single rainbow
gradient reads as a printed stripe, because real foil is never evenly lit. */
.holo__foil {
position: absolute;
inset: -20%;
background-image:
repeating-linear-gradient(
112deg,
rgba(255, 60, 172, 0.55) 0 22px,
rgba(255, 214, 92, 0.55) 22px 44px,
rgba(74, 255, 208, 0.55) 44px 66px,
rgba(96, 140, 255, 0.55) 66px 88px,
rgba(255, 60, 172, 0.55) 88px 110px
),
repeating-linear-gradient(
64deg,
rgba(255, 255, 255, 0.22) 0 14px,
rgba(0, 0, 0, 0.22) 14px 28px
);
/* Shifting the foil AGAINST the cursor, and much further than the cursor
moves, is the parallax that sells depth: the pattern appears to sit under
the surface rather than printed on it. */
background-position:
calc(var(--hx) * -140px) calc(var(--hy) * -140px),
calc(var(--hx) * 90px) calc(var(--hy) * 90px);
/* color-dodge blows out where the foil is bright and vanishes where it is
dark, which is exactly how foil behaves as it catches the light. */
mix-blend-mode: color-dodge;
opacity: 0.55;
transition: opacity 220ms ease;
pointer-events: none;
}
/* A soft glare that tracks the pointer, so there is a single light source the
tilt and the foil both answer to. */
.holo__glare {
position: absolute;
inset: 0;
background: radial-gradient(
34% 34% at calc(var(--hx) * 100%) calc(var(--hy) * 100%),
rgba(255, 255, 255, 0.55),
transparent 70%
);
mix-blend-mode: overlay;
pointer-events: none;
}
.holo:hover .holo__foil { opacity: 0.85; }
.holo__body {
position: relative;
z-index: 1;
height: 100%;
padding: 22px;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 6px;
color: #f4f1ff;
font-family: system-ui, sans-serif;
}
.holo__kicker {
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(226, 216, 255, 0.72);
}
.holo__title { font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
.holo__meta { font-size: 12.5px; line-height: 1.45; color: rgba(226, 216, 255, 0.7); }
@media (prefers-reduced-motion: reduce) {
.holo { transition: none; transform: none; }
.holo__foil { transition: none; }
}
`;
export function HolographicCard() {
const ref = useRef<HTMLElement>(null);
const track = useCallback((event: React.PointerEvent<HTMLElement>) => {
const card = ref.current;
if (!card) return;
const rect = card.getBoundingClientRect();
card.style.setProperty('--hx', String((event.clientX - rect.left) / rect.width));
card.style.setProperty('--hy', String((event.clientY - rect.top) / rect.height));
}, []);
const reset = useCallback(() => {
const card = ref.current;
if (!card) return;
card.style.setProperty('--hx', '0.5');
card.style.setProperty('--hy', '0.5');
}, []);
return (
<>
<style>{css}</style>
<article className="holo" ref={ref} onPointerMove={track} onPointerLeave={reset}>
<div className="holo__foil" />
<div className="holo__glare" />
<div className="holo__body">
<p className="holo__kicker">Series A · 014</p>
<h3 className="holo__title">Prism Foil</h3>
<p className="holo__meta">
Tilt it toward the light. The pattern moves against your cursor, so it reads as depth
rather than print.
</p>
</div>
</article>
</>
);
}