Skip to content

Repository files navigation

Wallet Card Kit

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

Run it

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 sources

Use it

import { 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.

Just the card

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).

Pieces, if you want to build your own layout

Stage, SwatchPicker, OptionGrid, Segmented, HintBar, SpecPanel are all exported individually and have no interdependencies beyond the stylesheet.

How the artwork works

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:

  1. 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.
  2. 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.

Accessibility

  • 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: reduce removes the sheen and all motion; the card still renders, flips and reads.
  • Everything is keyboard reachable; the hint bar is a real tablist.

Keyboard

Key Action
A / B / C Pigment / Woven / Color field
← → previous / next artwork palette
F flip the card

Notes

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.

About

Physical-feeling membership card for subscription pickers: marbled cover art, 3D tilt, pointer-tracked sheen, print-layer parallax, flip. React + CSS, no dependencies.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages