Cards
All cards: 24px radius, 24px inset, --surface on --canvas, 1px --border, no resting shadow.
14. BalanceCard#
The hero: one trusted number, and the reason a user opens the product.
Layer 1: radius 24 · inset 24 · min-width 280 · height auto
Layer 2:
┌─ 24 ─────────────────────────────────┐
│ [eyebrow] · [account mark] │ ← row, space-between
│ 8 │
│ [figure] │
│ 8 │
│ [delta] · [as-of] │
│ 16 │
├─ full-bleed sparkline ───────────────┤ ← optional, bleeds to 3 edges
└──────────────────────────────────────┘
Layer 3: eyebrow (1 line, truncate) · figure (never truncates) · delta (1 line) · media (aspect-[4/1], full-bleed) · actions (max 2)
Layer 4:
| Slot | Token | Weight | Color |
|---|---|---|---|
| eyebrow | label 13/16 |
500 | --text-secondary |
| figure | figure-hero 40/48 |
500 | --text-primary |
| delta | caption 12/16 |
400 | semantic |
3 sizes, 2 weights ✓
Available vs. current. A bank balance has two values and they differ. Show available as the figure, and current as a secondary line when they diverge. Mixing them up is the costliest copy error in consumer banking UI.
Polarity: required, no default. standard for a cash or asset balance, inverted for a card or credit line, where up means more owed. (API change: it previously fell back to standard.)
Motion: none at rest. Value flash when live. Sparkline draws once. Interactive variant gets press scale(0.97) and a 1px hover lift.
Responsive: figure steps 40 → 28 below a 280px container. Sparkline drops below 240px.
Edge cases: negative balance (overdraft — parens and a --negative figure, plus an explicit "Overdrawn" pill) · redacted · stale (dim to 0.7 + StaleIndicator) · very long account name.
15. KpiCard#
Label · figure · delta · optional sparkline. The dashboard atom.
- Baselines align across a
KpiRow. Usegridwithauto-rows-frand a shared figure row, or two cards with different label lengths put their figures at different heights. It's the most visible alignment failure on a dashboard. - Display family forbidden here. Figures align across the row.
polarityis required, with no default, to force the decision.- The comparison basis is always shown:
vs. Aug, never a bare+5.3%.
Loading: skeleton with the figure block at the exact figure width, or the card jumps when data lands.
16. AccountCard#
Bank account: institution mark, masked number, type, available and current balances, sync status. Tapping opens the transaction list filtered to it.
Shows disconnected and reauth-required states prominently, because a stale connection quietly showing an old balance is a real financial hazard. Stale accounts dim and offer a "Reconnect" action.
17. PaymentMethodCard#
The physical object. It gets two of the four sanctioned delight moments: the flip and the tilt.
Geometry, from ISO/IEC 7810 ID-1:
aspect-ratio: 1.586 (85.60 × 53.98 mm)
border-radius: 3.7% / 5.9% (3.18mm — 3.7% of width, 5.9% of height)
Real proportions matter. A 16:9 card with a 24px radius reads as a rectangle, not a card.
Networks: Visa, Mastercard, American Express, Discover, JCB, UnionPay and Diners Club. The
mark is CardNetworkMark unless a licensed one is passed
as networkLogo.
Face: flat, a single tint, network mark top-right opposite the chip, masked PAN in tnum with ss02 (so a 0 isn't an O), holder name in label uppercase, expiry MM/YY. Each network has a dark default tint close to the color a reader expects of it, and tint overrides it with the issuer's. Visa, Amex and Diners Club print their mark in the face's ink; the others print in color. The masked number follows the network's grouping: •••• •••••• •0005 for Amex, •••• •••••• 1667 for a 14-digit Diners Club.
Amex prints its four-digit code on the front, above the number on the right. An Amex card reveals the code in place and doesn't turn over, because flipping to an empty reverse teaches the holder to look in the wrong place. The other six flip to a three-digit code on the back.
No code, no control. A card on file keeps no security code after authorization. Without cvv the reveal button isn't rendered, since a flip to ••• reveals nothing.
Motion:
- Flip to reveal CVV —
rotateY(0→180deg),spring.playful, real 3D withpreserve-3d+backface-visibility: hidden. Not a crossfade. Spec. - Pointer tilt — ±6° from pointer position via
spring.track, plus a specular sheen atopacity 0→0.12. Desktop only, gated behind@media (hover: hover) and (pointer: fine). Springs home on leave. - Reduced motion: instant swap, no tilt.
The tilt is allowed only because the card is decorative. The same effect on a KpiCard fails review.
Security: CVV and full PAN are never in the DOM until revealed, never in analytics, never in a screenshot-able state without an explicit user action.
18. RunwayCard#
Months of runway, current cash, monthly burn, projected zero date. A burn line with a projection segment rendered dashed, because a projection is not a fact.
polarity="inverted" on burn. Runway under 6 months triggers --warning; under 3, --negative. The thresholds are configurable because they're a business judgment.
19. InvoiceCard#
Number, customer, amount, due date, aging state (current / 1-30 / 31-60 / 61-90 / 90+). Overdue is --negative with the day count, never color alone. Partial payments show paid vs. outstanding. A paid invoice — paidInFull, or derived from paid ≥ total (or nothing outstanding) in the invoice's currency — shows "Settled" and is never overdue, however far past its due date.
20. SubscriptionCard#
Vendor mark, amount, cadence, next charge, annualized cost. Annualized is the useful number. $49/mo is $588/yr, and that's the figure a spend review acts on.
21. AnomalyCard#
A flagged deviation, with the comparison that triggered it and one clear action (approve / flag / create rule). Never a bare alert. An anomaly without its baseline is noise.
API change: polarity is now required, no default (it previously defaulted to inverted). Spend anomalies pass inverted; a revenue or cash anomaly passes standard.
22. GoalCard#
Target vs. actual with a progress rule (a rule line, not a rounded progress bar, to match the statement grammar).
23. StatCard#
The minimal unit: label + value. 16px inset, no delta, no media. For dense grids where a KpiCard is too much.
181. CardStatement#
The issuer's statement summary: what's owed, by when, and how the figure was reached.
<CardStatement
issuer="Fernhill Bank Business Card"
network="visa"
last4="4242"
period={{ start: new Date(2026, 8, 1), end: new Date(2026, 8, 30) }}
previousBalance={money(384_210)}
paymentsAndCredits={money(-50_000)}
purchases={money(162_480)}
fees={money(0)}
interest={money(6_812)}
newBalance={money(503_502)}
minimumPayment={money(15_100)}
dueDate={new Date(2026, 9, 25)}
creditLimit={money(1_000_000)}
availableCredit={money(496_498)}
aprs={[{ label: "Purchases", rate: 24.99 }]}
lateFee={money(4_100)}
penaltyApr={29.99}
/>
Layer 2:
┌──────────────────────────────────────────────┐
│ [mark] Issuer •••• 4242 [Overdue] │ header: issuer, period, currency
├──────────────────────────────────────────────┤
│ New balance Minimum payment Due date │ the three figures a holder acts on
│ $5,035.02 $151.00 25 Oct 2026 │
│ ↑ +$1,192.92 vs previous statement │
├──────────────────────────────────────────────┤
│ Previous balance $3,842.10 │
│ Payments and credits ($500.00) │
│ Purchases $1,624.80 │
│ Fees charged — │
│ Interest charged $68.12 │
│ ────────── │
│ New balance $5,035.02 │
│ ══════════ │
├──────────────────────────────────────────────┤
│ Credit limit · Available credit · APRs │
├──────────────────────────────────────────────┤
│ Late payment warning. │
└──────────────────────────────────────────────┘
The walk ties or says so. Previous balance, payments and credits, purchases, fees and
interest are footed in minor units and compared with the new balance the issuer printed. A
non-zero difference adds a Difference row, turns the border --negative and states the amount.
A holder paying from a figure that doesn't add up should find out before paying.
Movements are signed, once. A payment reduces what's owed, so it's passed negative and
renders ($500.00) with the paren column reserved. There's no "−" beside the label, which would
make it a double negative. The component never negates.
Polarity is inverted. The change against the previous statement is a Delta with
polarity="inverted": a balance that rose is more owed. A credit balance (negative new balance)
is labelled "Credit balance", not printed as a negative debt.
States:
- Past due:
pastDuepresent and non-zero. AnOverduepill in the header, the due date in--negative, a line saying the past-due amount is included in the minimum, and the late-payment warning in--negativeinstead of the routine secondary text. - Over limit: derived from new balance vs. credit limit, with the amount over stated.
availableCreditis the issuer's figure, never recomputed, andnullrenders n/a. - Paid in full: derived when payments and credits cleared the balance brought forward. It's a fact the figures prove, so it's stated in one secondary line.
Late payment warning prints only when lateFee is given, built from the props (due date,
late fee, penalty APR) and never from invented terms. APRs print at two decimals.
Every figure states its basis: issuer, masked account, statement period and currency in the header; the due date is a date, not "in 12 days".
3 sizes, 2 weights: caption (meta and labels), body / figure-sm (the walk), figure-md
(the three figures acted on).
Motion: none. A statement is a document.