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.
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 makes a flat gradient read as light hitting foil
instead of a color painted over the card.
The mask-image does the rest: 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, with the same normalized pointer and the 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, on top
of the 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 travels with the tilt, 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 shift together as it tilts.
The holo effect borrows the spring, adds a CSS variable, and masks a gradient to it.
- Base
- dark radial gradient that 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 that 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.
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 makes a flat gradient read as light hitting foil
instead of a color painted over the card.
The mask-image does the rest: 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, with the same normalized pointer and the 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, on top
of the 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 travels with the tilt, 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.
GodUI
Move your pointer across the card.
The result
Move your pointer across each card. The foil, the glitter, and the glare shift together as it tilts.
The holo effect borrows the spring, adds a CSS variable, and masks a gradient to it.
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