Holographic card
NewA dark card whose foil blooms in rainbow light where the pointer moves, over contour lines and a fractal triangle, as the card tilts.
holographic-card/holographic-card.tsxHolographic
Light, computed.
Move the pointer over the card, or use the arrow keys, to light it.
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- A hero object that rewards a closer look: a limited edition, a launch, a membership tier, a collectible.
- A drop or a reveal page where one card is the whole stage and the reader is invited to play with the light.
- Showing off a brand’s craft: the foil, the contours and the fractal are computed live, so they stay crisp at any size.
- A dark, quiet surface whose interest appears only on interaction: at rest the card is nearly plain.
When not to use
- Content that has to be read at rest. The foil appears only under the light; put key facts in plain text beside the card.
- A grid of many cards. Each one is a WebGL canvas with its own context; use static images or a plain card and keep the foil for one.
- A card with an action on it. It is a focusable group, not a button; use a card with a real button or link.
- A product card for real merchandise or a payment card. Use a photo or a flat card; the foil reads as decoration, not as the thing.
- A small slot in a dense layout. It needs a stage of about 440 × 620 px to breathe; below that, use a thumbnail.
You can add this component with the shadcn CLI or by hand:
Into components/holographic-card of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/holographic-card.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
import { HolographicCard } from './holographic-card';export function EditionDrop() { return ( <HolographicCard brand="prism" title="Holographic" subtitle="Light, computed." edition="Edition 001" badge="Shader" autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
A collectible-style card, near-black at rest with a thin triangle on it. Bring the pointer near and a rainbow foil lights up around it: fine contour lines ringing the card, a Sierpinski triangle inside the outline, and a warm glare where the light is strongest. The card tilts away under the light and settles flat again when the pointer leaves. It is drawn by a WebGL shader, with a CSS foil where WebGL is missing.
- One WebGL2 fragment shader draws the whole foil on a canvas inside the card, in the card’s own units (442 × 628), so it looks the same at any size. The words, the triangle’s outline and the marks are HTML and SVG on top, and stay sharp.
- The contour lines are drawn every 8.5 units along a height field: the smooth minimum of the distances to two points, one right of the triangle and one under it, bent by a few slow sine waves. So they ring those points, merge, and run in wavy bands to the edges. Their width comes from the field’s screen-space derivative, so they stay about one CSS pixel at any scale.
- The colour is a rainbow laid diagonally across the card, from green at the lower left through blue, violet and pink to gold at the upper right, and it slides as the light moves, as a foil’s colours do when it tilts. The light is a soft round falloff about 360 units wide with a bright core; high on the card the core turns into a warm white glare.
- Inside the triangle, each pixel walks the Sierpinski subdivision from its barycentric coordinates: the kept triangles light up, the holes stay dark with a soft shade under their top edge, and levels smaller than about 2.5 device pixels are drawn as their average, so the fractal never shimmers.
- The light follows the pointer anywhere over the stage with a 140 ms lag, comes on in about 0.3 s and goes off in under 0.1 s when the pointer leaves. The card tilts away under it, up to 4.8° about its upright axis and 4.4° about its level one, with perspective.
- Frames run only while something moves and the stage is on screen in a visible tab. The canvas draws at most 2 device pixels per CSS pixel, 1.25 on a device with four cores or 4 GB of memory or less.
- Without WebGL2, or if the context is lost, a CSS foil stands in: the same rainbow seen through rings, under a round mask that follows the light through custom properties. Under reduced motion nothing tilts or eases; a still sheen rests on the card and the pointer moves the light directly.
- The card is sized in em of a font size the stage works out from its own width and height, so it scales down to fit a phone or a short stage. The stage and the hint are light-dark() tokens; the card stays dark in both schemes.
| Prop | Type | Default | Description |
|---|---|---|---|
brand | string | 'prism' | The wordmark at the top left of the card. |
title | string | 'Holographic' | The card’s title, low on the card. |
subtitle | string | 'Light, computed.' | The line under the title. |
edition | string | 'Edition 001' | The small print at the bottom left, set in capitals. |
badge | string | 'Shader' | The small print at the bottom right, set in capitals. |
caption | string | 'Move to reveal' | The hint under the card; an empty string leaves it out. |
deviceTilt | boolean | true | On a phone, the first touch asks to follow the phone’s tilt with the light, where the browser allows it. |
autoplay | boolean | true | While on screen and untouched, a demo pointer sweeps across the card, around it and away. Any real input hands control to the reader. |
defaultLight | { x: number; y: number } | — | Opens with the light held at this point, in card units (442 × 628, from the top left), the card tilted toward it, and the demo off. The reader’s input still moves it. |
colorScheme | 'light' | 'dark' | 'system' | — | The stage’s colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme. The card stays dark either way. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useId, useRef, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent,} from 'react';import { CARD, createFoil, type Foil } from './renderer';import { useAutoplay } from './use-autoplay';import styles from './holographic-card.module.css';/* * Holographic card: a dark card with a triangle on it. Move over it and a rainbow foil blooms * where the light falls: contour lines ringing the card, a Sierpinski triangle inside the * triangle, and the card tilts a little toward the light. * * - The foil is a WebGL2 shader (renderer.ts) on a canvas inside the card; the words, the outline * of the triangle and the marks are HTML and SVG on top, so they stay sharp and readable. * - The light follows the pointer anywhere over the stage, so it reaches the card's edge before * the pointer does. It eases after the pointer (about 60 ms), comes on in about 0.3 s and goes/* Holographic card (holographic-card.tsx): a near-black stage with the card in its middle and a quiet hint under it; by day the stage turns pale and the card stays dark, as an object would. Sizes. The card is drawn at 442 × 628 px at its own size: every length inside it is in em of a 16 px font size, which the stage sets from its own size (a container query), so the whole card scales down to fit a smaller or shorter stage and keeps its proportions. Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes the color-scheme it inherits from the page, or the one its colorScheme prop sets. Memory. The stage is one layout and size boundary; the card tilts by a transform on its own layer, the light is a canvas the shader redraws, and the demo pointer moves by a transform. */.root { /* The stage, its glow behind the card, and the hint's words. */ --stage: #06090a; --stage-glow: #0b0e10; --hint: rgb(255 255 255 / 0.3); /* The card's shadow on the stage. */ --card-shadow: rgb(0 0 0 / 0.85); /* The same on both schemes: the card itself, which stays dark. */ --card-top: #111315; --card-bottom: #0f1112; --card-edge: rgb(255 255 255 / 0.13);// Copyright (c) 2026 Bruno Delorence. MIT License./* * The foil of the holographic card: one WebGL2 fragment shader over the whole card, in the card's * own design units (442 × 628, y down), whatever size it is drawn at. * * - The card's ground: near-black, a shade darker toward the bottom. With no light, the shader * draws only this, so the card at rest is plain. * - Contour lines: thin lines every 8.5 units along a height field, the smooth minimum of the * distances to two points (one right of the triangle, one under it), bent by a few slow waves. * So the lines ring those two points, merge, and run in long wavy bands to the edges. * - The colour is a rainbow laid diagonally across the card (green at the lower left, through * blue, violet and pink, to gold at the upper right) that slides a little as the light moves * across, as a foil's colours do when it tilts. * - The light: a soft round falloff about 360 units wide around the pointer, brightest in a small * core, which turns the lines on in their colour and washes the ground between them. Far from it * the lines stay barely visible while the light is on at all. * - The triangle: a 1-unit outline, an 8-unit band around it with no lines, and inside, a * Sierpinski triangle walked level by level from the point's barycentric coordinates. The kept * triangles light up in the foil's colour; the holes stay dark, with a soft shade under their top * edge. Levels smaller than about 2.5 device px are drawn as their average, so they never shimmer. */export const CARD = { width: 442, height: 628 } as const;'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver( ([entry]) => { onScreen = entry.isIntersecting;- The card is a focusable group named by its title, subtitle and edition, described by a hidden hint. With focus, the arrow keys move the light (Shift moves it further), Home centres it and Escape turns it off; blur turns it off too. The focus ring is drawn around the card.
- The canvas, the CSS foil, the triangle, the marks, the hint under the card and the demo pointer are decorative and hidden from screen readers; the words on the card are real text.
- On touch screens, a sideways drag moves the light while an up-and-down one still scrolls the page. Following the phone’s tilt starts only after a touch, and only if the browser grants it.
- Under reduced motion the card never tilts, the light does not ease, the demo never plays, and a still sheen shows what the foil looks like.