A physical-feeling membership card for subscription and plan pickers: the user chooses a tier and the details, and a real card on the bench updates live.
Everything is self-contained React + CSS. No 3D library, no image assets, no fonts to ship — the cover artwork is generated on the fly in a WebGL shader.
src/
WalletCard.tsx the card: tilt, sheen, print-layer parallax, flip
useTilt.ts pointer -> damped rotation, reduced-motion aware
marbling.ts procedural marbled/ink artwork (GPU -> data URL, cached)
textures.ts paper grain, halftone, weave, printed grid (inline SVG)
controls.tsx tier segmented control, artwork swatches, option grid,
keyboard hint bar, stage
SpecPanel.tsx the right-hand specification column
PlanStudio.tsx composes all of the above into the full picker
styles.css everything, scoped under .wck-*
demo/ three scenarios you can flip between
npm install
npm run dev # demo on vite
npm run build # library -> dist/ (ESM + .d.ts + wallet-card-kit.css)
npm run build:demo # static demo -> dist-demo/On this machine, heavy builds run on the build host instead of the laptop:
./scripts/build-remote.sh # typecheck + library build
./scripts/build-remote.sh --demo # ...plus the static demo
./scripts/build-remote.sh --shell # container with the synced sourcesimport { PlanStudio } from 'wallet-card-kit';
import 'wallet-card-kit/styles.css';
<PlanStudio
cardName="Ian's family"
cardSince="Since May 12, 2026"
tiers={tiers} // plan tiers, each with its own detail options
metrics={metrics} // the coloured ledger in the right column
total={2186}
/>;tiers drives the whole page — the plan buttons, the detail options, the
action label, the headline and the numbers beside the card all read from it.
Swap the array for your real catalogue and the interaction works unchanged.
import { WalletCard, PALETTES } from 'wallet-card-kit';
<WalletCard
brand={{ name: "Ian's family", since: 'Since May 12, 2026', badge: 'LIFETIME PRO' }}
palette={PALETTES[4]}
treatment="pigment"
tilt={11}
/>;Props worth knowing: variant (cover art-first / plan data-first),
flipped + onFlip (controlled), flippable, tilt (0 disables motion),
glare, elevation, backContent, seed (changes the artwork variation).
Stage, SwatchPicker, OptionGrid, Segmented, HintBar, SpecPanel are
all exported individually and have no interdependencies beyond the stylesheet.
coverBackground(palette, treatment, w, h, seed) renders one card face in a
fragment shader and returns a data URL, which is then cached by key:
- a domain-warped fBm field gives the ink-in-water wash;
- an anisotropic stretch turns it into the long fibres of marbled paper;
- a second independent colour field pulls a different set of palette hues into the same card, so a cover reads as several pigments bleeding together rather than one tint;
- a periodic band along the warp field paints the thin combed veins;
- fine noise and directional striations supply the paper grain;
- a directional ramp keeps the light source consistent with the tilt.
treatment picks one of three presets — pigment (ink in water), woven
(pressed into cloth: shorter fibres, more grain) and field (broad flat
colour). seed re-rolls the pattern without changing the palette.
Two consequences worth knowing:
- Rendering happens once, not per frame. The card is then pure CSS transforms and gradients, so tilt and flip stay cheap and the GPU is not re-running noise every frame.
- The shader context is thrown away immediately after each rasterise
(
WEBGL_lose_context). Browsers cap live WebGL contexts; a grid of twelve swatches would blow through that limit otherwise.
If WebGL is unavailable the generator returns a layered radial-gradient
stack built from the same palette, so the layout never breaks.
- The card is a real button: focusable,
aria-pressed, Enter/Space flips it. - A pointer-down/up pair is treated as a click only if it did not move more than 8px or last longer than 900ms, so dragging across the card does not flip it.
prefers-reduced-motion: reduceremoves the sheen and all motion; the card still renders, flips and reads.- Everything is keyboard reachable; the hint bar is a real tablist.
| Key | Action |
|---|---|
A / B / C |
Pigment / Woven / Color field |
← → |
previous / next artwork palette |
F |
flip the card |
All code here is written from scratch; no third-party components or their CSS were copied. The visual target (Apple Card-style physicality, and the Readmate / Cali Baby family-card studies) is a native-app look; this is an independent web interpretation of the interaction.
The survey of existing libraries (tilt, foil/sheen, cover artwork, flip) and why none of them is wrapped, with licenses and the pitfalls that come with them, is in docs/prior-art.md.