A collectible card that behaves like printed foil. It tilts in 3D towards the pointer, a rainbow band blended with color-dodge slides across the illustration as the angle changes, a fine dot screen sits over the art and a soft white glare follows the pointer. The name runs vertically down one edge and is mirrored on the opposite corner like a playing card. Left alone, the card sways slowly so the foil is always moving. The demo pairs it with a product blurb, three facts and two buttons. On phones the card sits above the text and tilts under a finger.
// Adapted from Eldora UI (https://github.com/karthikmudunuri/eldoraui/blob/main/apps/www/registry/eldoraui/holographic-card.tsx)
// Copyright (c) Mudunuri Bhaskara Karthikeya Varma. MIT License: https://we.inc/components/licenses
import { useRef, useState } from "react";
import type { PointerEvent } from "react";
import { motion, useAnimationFrame, useMotionTemplate, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
const card = {
name: "TIDEWALKER",
number: "No. 017 / 250",
series: "Deepwater series",
rarity: "Foil rare",
accent: "#fde047",
};
const stats = [
{ label: "Edition", value: "250 printed" },
{ label: "Finish", value: "Rainbow foil" },
{ label: "Ships", value: "In a rigid sleeve" },
];
const FOIL = "linear-gradient(125deg, transparent 28%, rgba(255,0,140,0.55) 40%, rgba(255,214,0,0.45) 47%, rgba(0,255,220,0.55) 54%, rgba(90,120,255,0.5) 61%, transparent 72%)";
/** The card art: a moon, a tide and a walking figure, drawn in SVG so nothing is loaded. */
function Art() {
return (
<svg viewBox="0 0 320 460" className="absolute inset-0 h-full w-full" aria-hidden="true">
<defs>
<linearGradient id="holo-card-sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#1e1b4b" />
<stop offset="0.55" stopColor="#4c1d95" />
<stop offset="1" stopColor="#0f172a" />
</linearGradient>
<radialGradient id="holo-card-moon" cx="0.4" cy="0.35">
<stop offset="0" stopColor="#fff7ed" />
<stop offset="1" stopColor="#fda4af" />
</radialGradient>
</defs>
<rect width="320" height="460" fill="url(#holo-card-sky)" />
<circle cx="160" cy="170" r="150" fill="#a78bfa" opacity="0.18" />
<circle cx="160" cy="170" r="104" fill="#f0abfc" opacity="0.2" />
<circle cx="160" cy="170" r="68" fill="url(#holo-card-moon)" />
<circle cx="138" cy="150" r="9" fill="#fb7185" opacity="0.35" />
<circle cx="182" cy="192" r="14" fill="#fb7185" opacity="0.28" />
{[0, 1, 2, 3, 4].map((i) => (
<path
key={i}
d={`M -20 ${292 + i * 34} q 45 ${-22 + i * 2} 90 0 t 90 0 t 90 0 t 90 0 v 200 h -360 z`}
fill={["#312e81", "#3730a3", "#1e3a8a", "#172554", "#0b1120"][i]}
opacity={0.92}
/>
))}
<g transform="translate(160 262)" fill="#0b1120">
<circle cx="0" cy="-34" r="9" />
<path d="M -9 -24 h 18 l 5 34 h -9 l 2 30 h -8 l -3 -26 l -3 26 h -8 l 2 -30 h -9 z" />
<path d="M 12 -20 l 22 -40 l 3 2 l -20 44 z" />
</g>
{[[40, 60], [270, 44], [240, 120], [62, 150], [286, 210], [30, 236], [118, 40], [210, 30]].map(([x, y], i) => (
<circle key={i} cx={x} cy={y} r={i % 3 === 0 ? 1.8 : 1.1} fill="#fff" opacity={0.85} />
))}
</svg>
);
}
export default function HolographicFoilCollectibleCardWithTiltAndGlare() {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const [held, setHeld] = useState(false);
// Pointer position over the card, from -0.5 to 0.5 on each axis, smoothed by springs.
const x = useMotionValue(0);
const y = useMotionValue(0);
const sx = useSpring(x, { stiffness: 180, damping: 22 });
const sy = useSpring(y, { stiffness: 180, damping: 22 });
const rotateX = useTransform(sy, [-0.5, 0.5], [13, -13]);
const rotateY = useTransform(sx, [-0.5, 0.5], [-13, 13]);
const foilX = useTransform(sx, [-0.5, 0.5], ["0%", "100%"]);
const foilY = useTransform(sy, [-0.5, 0.5], ["0%", "100%"]);
const glareX = useTransform(sx, [-0.5, 0.5], ["0%", "100%"]);
const glareY = useTransform(sy, [-0.5, 0.5], ["0%", "100%"]);
const glare = useMotionTemplate`radial-gradient(circle at ${glareX} ${glareY}, rgba(255,255,255,0.55) 0%, transparent 55%)`;
const shadowX = useTransform(sx, [-0.5, 0.5], [22, -22]);
const shadow = useMotionTemplate`${shadowX}px 34px 60px -18px rgba(76,29,149,0.75)`;
// With no pointer on it, the card sways slowly so the foil keeps moving.
useAnimationFrame((t) => {
if (held || reduce) return;
x.set(Math.sin(t / 1500) * 0.34);
y.set(Math.cos(t / 2100) * 0.26);
});
const onMove = (e: PointerEvent<HTMLDivElement>) => {
const r = ref.current?.getBoundingClientRect();
if (!r) return;
setHeld(true);
x.set((e.clientX - r.left) / r.width - 0.5);
y.set((e.clientY - r.top) / r.height - 0.5);
};
const vertical = { writingMode: "vertical-rl" as const };
return (
<section className="overflow-hidden bg-[#0d0a1f] px-4 py-16 text-white sm:px-6 sm:py-24">
<div className="mx-auto grid max-w-5xl items-center gap-14 lg:grid-cols-[auto_1fr] lg:gap-20">
<div className="mx-auto [perspective:1200px]">
<motion.div
ref={ref}
onPointerMove={onMove}
onPointerDown={onMove}
onPointerLeave={() => setHeld(false)}
onPointerCancel={() => setHeld(false)}
initial={reduce ? false : { opacity: 0, y: 40, scale: 0.92 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ type: "spring", stiffness: 90, damping: 16 }}
style={{ rotateX, rotateY, boxShadow: shadow, transformStyle: "preserve-3d" }}
className="relative h-[414px] w-[288px] touch-none select-none rounded-3xl sm:h-[460px] sm:w-[320px]"
>
<div className="absolute inset-0 overflow-hidden rounded-3xl border border-white/15 bg-black">
<Art />
{/* Fine dot screen, like the print pattern on a real foil card. */}
<div className="pointer-events-none absolute inset-0 opacity-25 [background-image:radial-gradient(circle,#000_1px,transparent_1px)] [background-size:3px_3px]" />
{/* The foil: a rainbow band that slides across as the card tilts. */}
<motion.div
className="pointer-events-none absolute inset-0 mix-blend-color-dodge"
style={{ backgroundImage: FOIL, backgroundSize: "220% 220%", backgroundPositionX: foilX, backgroundPositionY: foilY, opacity: 0.75 }}
/>
<div className="pointer-events-none absolute inset-0 p-5">
<p
className="absolute left-5 top-5 text-2xl font-bold tracking-[0.12em] transition-[color,text-shadow] duration-300"
style={{ ...vertical, color: held ? card.accent : "#fff", textShadow: held ? `0 0 16px ${card.accent}` : "none" }}
>
{card.name}
</p>
<p
className="absolute bottom-5 right-5 rotate-180 text-2xl font-bold tracking-[0.12em] transition-[color,text-shadow] duration-300"
style={{ ...vertical, color: held ? card.accent : "#fff", textShadow: held ? `0 0 16px ${card.accent}` : "none" }}
aria-hidden="true"
>
{card.name}
</p>
<p className="absolute right-5 top-5 rounded-full bg-black/45 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-widest">{card.rarity}</p>
<p className="absolute bottom-5 left-5 text-xs font-medium leading-relaxed text-white/90">
{card.series}
<br />
<span className="font-mono">{card.number}</span>
</p>
</div>
</div>
{/* Glare sits above everything and follows the pointer. */}
<motion.div className="pointer-events-none absolute inset-0 rounded-3xl mix-blend-overlay" style={{ background: glare }} />
</motion.div>
</div>
<div className="text-center lg:text-left">
<p className="text-sm font-semibold uppercase tracking-widest text-fuchsia-300">Drop 04, Friday at noon</p>
<h2 className="mt-4 text-4xl font-semibold tracking-tight sm:text-5xl">Cards that catch the light, even on a screen</h2>
<p className="mx-auto mt-5 max-w-lg text-lg leading-relaxed text-violet-100/75 lg:mx-0">
Pocket Atlas prints small runs of illustrated cards with a real foil layer. Tilt this one with your mouse or finger to see how it moves.
</p>
<dl className="mx-auto mt-9 grid max-w-lg grid-cols-3 gap-px overflow-hidden rounded-2xl bg-white/10 text-left lg:mx-0">
{stats.map((s) => (
<div key={s.label} className="bg-[#151030] p-4">
<dt className="text-xs uppercase tracking-wider text-violet-200/70">{s.label}</dt>
<dd className="mt-1.5 text-sm font-semibold leading-snug">{s.value}</dd>
</div>
))}
</dl>
<div className="mt-9 flex flex-wrap justify-center gap-3 lg:justify-start">
<a href="#" className="inline-flex h-12 items-center rounded-full bg-white px-7 text-sm font-semibold text-violet-950 transition-colors hover:bg-yellow-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-300 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d0a1f]">
Reserve for $24
</a>
<a href="#" className="inline-flex h-12 items-center rounded-full border border-white/20 px-7 text-sm font-semibold text-white transition-colors hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d0a1f]">
See the full set
</a>
</div>
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/holographic-foil-collectible-card-with-tilt-and-glare.json
A diagonal gradient with pink, yellow, cyan and blue stops is laid over the art at more than twice the card's size, with `mix-blend-mode: color-dodge`. As the pointer moves, the gradient's background position follows it, so the band travels across the card and brightens whatever is underneath.
Real foil cards are printed through a halftone screen. The 3px dot layer breaks up the gradient and the artwork slightly, which stops the foil looking like a smooth plastic overlay.
Yes. It listens to pointer events, so pressing and dragging on the card tilts it. The card sets `touch-action: none` so the drag is not treated as a page scroll while the finger is on it.
Yes. Put an image where `Art` is rendered. Color-dodge is strongest over mid and dark tones, so very bright photos will wash out: lower the foil opacity or darken the image.
The card's shadow shifts sideways opposite to the tilt, which is what a lifted object does under a fixed light. It is a small detail that makes the 3D rotation read as real depth.
Adapted from Holographic Card in Eldora UI, used under its open-source licence. Licences and credits
Related: 3D Tilt Card with Parallax Layers and Glare, Card with Diagonal Glare Sweep on Hover, Spotlight Cards with Pointer Light.
More animated cards or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.