Anatomy of Holographic Card
How the same spring that tilts Tilt Card also slides an iridescent foil across a dark base, and why one blend mode makes it read as light instead of paint.
↓Scroll to step through it
Four masks, one blend mode
Holographic Card reuses Tilt Card's entire pointer-to-spring pipeline — same normalized coordinates, same spring constants — and spends its own complexity on one thing: making the tilt also drive a foil that looks like it's catching real light. Here's the layer stack and the blend mode that sells it.
The card is a dark base with three absolute inset-0 layers stacked on top:
a diagonal foil gradient, a glitter dot mask, and a soft radial glare —
foil and glitter both use mix-blend-color-dodge, which lightens the dark
base wherever the layer above it is bright and leaves it untouched where the
layer is dark. That's what makes a flat gradient read as light hitting foil
instead of a color painted over the card.
The mask-image is the other half of the trick: the foil is masked to a
radial gradient centered on the same pointer position, so the iridescence
concentrates where the "light" is hitting rather than flooding the whole
card evenly.
The tilt is borrowed wholesale. rotateX/rotateY come from the identical
spring pipeline as Tilt Card — same normalized pointer, same
{ stiffness: 170, damping: 12, mass: 0.1 } spring:
const rotateX = useTransform(sy, [-0.5, 0.5], [maxTilt, -maxTilt]);
const rotateY = useTransform(sx, [-0.5, 0.5], [-maxTilt, maxTilt]);What's new is that the same sx/sy pair also feeds a CSS variable instead
of just a rotation:
const holoX = useTransform(sx, [-0.5, 0.5], ["0%", "100%"]);
const holoY = useTransform(sy, [-0.5, 0.5], ["0%", "100%"]);
// …
style={{ rotateX, rotateY, "--holo-x": holoX, "--holo-y": holoY }}The foil that catches the light
--holo-x/--holo-y land on background-position for the foil, the
sparkle mask, and the glare's radial-gradient center all at once. One
pointer value moves the card and slides the foil across it in the same
gesture — turn the card and the color visibly travels with the tilt, exactly
like a real trading-card holo catching the light at an angle.
Each variant swaps the gradient stops but keeps the mechanism identical.
Optional: the gyroscope. On a touch device there's no pointer to move, so
gyroscope opts into reading deviceorientation instead — gamma/beta
map onto the same px/py motion values the pointer would have set. iOS
gates the sensor behind a permission prompt that must fire from a user
gesture, so the listener requests it lazily on the first tap rather than on
mount:
const requestOnTap = () => {
orientationEvent.requestPermission?.().then((state) => {
if (state === "granted") attach();
});
};
if (typeof orientationEvent.requestPermission === "function") {
window.addEventListener("pointerdown", requestOnTap);
} else {
attach();
}Because it writes into the same px/py values as the pointer handler, the
spring, the tilt, and the foil don't need to know which input drove them.
The result
Move your pointer across each card — the foil, the glitter, and the glare all shift together as it tilts.
Borrow the spring, add a CSS variable, mask a gradient to it — that's the whole holo effect.
- Base
- dark radial gradient — seats the foil so it reads as light
- Foil
- diagonal gradient, mix-blend-color-dodge
- Sparkle
- glitter dot mask, same blend mode
- Glare
- soft radial highlight, tracks the pointer
const FOIL_LAYER ="pointer-events-none absolute inset-0 mix-blend-color-dodge " +"[background-size:200%_200%] [background-position:var(--holo-x)_var(--holo-y)] " +"[mask-image:radial-gradient(75%_75%_at_var(--holo-x)_var(--holo-y),#000_0%,rgba(0,0,0,0.4)_55%,transparent_100%)]";Anatomy of Holographic Card
How the same spring that tilts Tilt Card also slides an iridescent foil across a dark base, and why one blend mode makes it read as light instead of paint.
- Base
- dark radial gradient — seats the foil so it reads as light
- Foil
- diagonal gradient, mix-blend-color-dodge
- Sparkle
- glitter dot mask, same blend mode
- Glare
- soft radial highlight, tracks the pointer
const FOIL_LAYER ="pointer-events-none absolute inset-0 mix-blend-color-dodge " +"[background-size:200%_200%] [background-position:var(--holo-x)_var(--holo-y)] " +"[mask-image:radial-gradient(75%_75%_at_var(--holo-x)_var(--holo-y),#000_0%,rgba(0,0,0,0.4)_55%,transparent_100%)]";Four masks, one blend mode
Holographic Card reuses Tilt Card's entire pointer-to-spring pipeline — same normalized coordinates, same spring constants — and spends its own complexity on one thing: making the tilt also drive a foil that looks like it's catching real light. Here's the layer stack and the blend mode that sells it.
The card is a dark base with three absolute inset-0 layers stacked on top:
a diagonal foil gradient, a glitter dot mask, and a soft radial glare —
foil and glitter both use mix-blend-color-dodge, which lightens the dark
base wherever the layer above it is bright and leaves it untouched where the
layer is dark. That's what makes a flat gradient read as light hitting foil
instead of a color painted over the card.
The mask-image is the other half of the trick: the foil is masked to a
radial gradient centered on the same pointer position, so the iridescence
concentrates where the "light" is hitting rather than flooding the whole
card evenly.
The tilt is borrowed wholesale. rotateX/rotateY come from the identical
spring pipeline as Tilt Card — same normalized pointer, same
{ stiffness: 170, damping: 12, mass: 0.1 } spring:
const rotateX = useTransform(sy, [-0.5, 0.5], [maxTilt, -maxTilt]);
const rotateY = useTransform(sx, [-0.5, 0.5], [-maxTilt, maxTilt]);What's new is that the same sx/sy pair also feeds a CSS variable instead
of just a rotation:
const holoX = useTransform(sx, [-0.5, 0.5], ["0%", "100%"]);
const holoY = useTransform(sy, [-0.5, 0.5], ["0%", "100%"]);
// …
style={{ rotateX, rotateY, "--holo-x": holoX, "--holo-y": holoY }}background-position: var(--holo-x) var(--holo-y)
- Tilt
- rotateX/rotateY from the shared spring
- Foil
- same sx/sy, mapped to a background position
const FOIL: Record<HolographicVariant, string> = {rainbow: "[background-image:linear-gradient(115deg,#ff2d95,#ffd84d,#4dff9e,#4dd2ff,#a24dff,#ff2d95)]",aurora: "[background-image:linear-gradient(115deg,#22d3ee,#34d399,#a3e635,#38bdf8,#22d3ee)]",galaxy: "[background-image:linear-gradient(115deg,#a855f7,#ec4899,#3b82f6,#c026d3,#a855f7)]",gold: "[background-image:linear-gradient(115deg,#b45309,#fbbf24,#fffbeb,#fbbf24,#b45309)]",};The foil that catches the light
--holo-x/--holo-y land on background-position for the foil, the
sparkle mask, and the glare's radial-gradient center all at once. One
pointer value moves the card and slides the foil across it in the same
gesture — turn the card and the color visibly travels with the tilt, exactly
like a real trading-card holo catching the light at an angle.
Each variant swaps the gradient stops but keeps the mechanism identical.
Optional: the gyroscope. On a touch device there's no pointer to move, so
gyroscope opts into reading deviceorientation instead — gamma/beta
map onto the same px/py motion values the pointer would have set. iOS
gates the sensor behind a permission prompt that must fire from a user
gesture, so the listener requests it lazily on the first tap rather than on
mount:
const requestOnTap = () => {
orientationEvent.requestPermission?.().then((state) => {
if (state === "granted") attach();
});
};
if (typeof orientationEvent.requestPermission === "function") {
window.addEventListener("pointerdown", requestOnTap);
} else {
attach();
}Because it writes into the same px/py values as the pointer handler, the
spring, the tilt, and the foil don't need to know which input drove them.
Myndui
Move your pointer across the card.
The result
Move your pointer across each card — the foil, the glitter, and the glare all shift together as it tilts.
Borrow the spring, add a CSS variable, mask a gradient to it — that's the whole holo effect.
Accessibility
Under prefers-reduced-motion, the card renders still — no spring, no
pointer listener, no gyroscope — with the foil and glare pinned to a fixed
resting position instead of centered dynamically:
if (reduceMotion) {
return (
<div style={{ "--holo-x": "50%", "--holo-y": "40%" }}>
{/* base, foil, children, edge — no glare, no motion */}
</div>
);
}Motion Score
transformPointer tiltbackground-positionHolographic sheen gradient follow