Primitives
Build these first. Everything above them inherits their mistakes.
1. Money
The core primitive. Every monetary value in the system renders through it.
<Money value={{ amount: 124056, currency: "USD" }} /> // $1,240.56
<Money value={{ amount: -124056, currency: "USD" }} accounting /> // ($1,240.56)
<Money value={null} /> // n/a
<Money value={{ amount: 0, currency: "USD" }} statement /> // —
interface MoneyProps extends React.ComponentPropsWithoutRef<"span"> {
value: MoneyValue | null;
/** parentheses for negatives (statements/tables) vs. a leading − (deltas) */
accounting?: boolean;
/** em dash for a true zero — statements only */
statement?: boolean;
size?: "xs" | "sm" | "md" | "lg" | "hero";
/** subordinate the symbol; default true */
showSymbol?: boolean;
/** show the ISO code — required in mixed-currency contexts */
showCode?: boolean;
/** mark as converted: renders "≈" and requires a rate for the tooltip */
approximate?: { rate: number; asOf: Date; from: string };
redacted?: boolean;
locale?: string;
}
Layer 3 — structure: [≈] [symbol] [integer] [.decimals] [code] — five slots, four optional. The paren column is reserved on both sides regardless of sign.
Layer 4 — type: one size, one weight. tnum + case always. Symbol and code at --text-tertiary; decimals at --text-tertiary only at lg/hero.
Motion: none, except the value flash when live and the crossfade when the value changes from a user action. Never interpolates.
Edge cases: JPY 0dp · KWD 3dp · bigint amounts · negative zero (renders as zero) · the widest-plausible-value width reservation · aria-label announcing the sign in words.
Why it matters: if this is wrong, everything is wrong. It also carries the whole case/tnum/paren-alignment craft that the rest of the library assumes.
2. Delta
A signed change. Never colored without a redundant cue.
<Delta value={{ amount: 12400, currency: "USD" }} percent={5.3} polarity="standard" />
// ↑ +$12,400.00 (+5.3%)
<Delta value={{ amount: 8200, currency: "USD" }} polarity="inverted" />
// ↑ +$8,200.00 ← colored NEGATIVE, because up is bad for this metric
interface DeltaProps {
value: MoneyValue | null;
percent?: PercentChange; // { kind: "points" | "relative"; value: number }
polarity?: "standard" | "inverted" | "neutral";
showArrow?: boolean; // default true
showPercent?: boolean;
basis?: string; // "vs. Aug" — never omit the comparison basis
}
The polarity contract is the whole point. A -4.2% change in burn is good. Defaulting everything to standard is how dashboards report cheerful green on a cost overrun.
Structure: [arrow] [sign] [value] [(percent)] [basis]. Sign glyph slot reserved so a column of deltas aligns even when some are unchanged.
Zero delta: — with --neutral-value, no arrow. Not +0.0%.
Motion: none. Value flash when live.
3. Figure
A large display numeral. The only place a figure may leave the figure face.
<Figure value={{ amount: 24819024, currency: "USD" }} size="hero" family="display" />
The step-down: at hero (40px), a value exceeding a character threshold steps to lg (28px), then md (20px). Driven by a container query on character count, not clamp() — so the value always lands on a type-scale token rather than between two.
The display gate: family="display" is only valid when the component is provably solitary. In KpiRow, where figures align across cards, the prop is ignored and the figure face is forced. Enforce it in the parent, not by trusting the caller.
Never truncates. Escalation ladder in numerics.
4. Percent
Percent and percentage points are different numbers. This component makes conflating them impossible.
<Percent value={{ kind: "relative", value: 25.0 }} /> // +25.0%
<Percent value={{ kind: "points", value: 5.0 }} /> // +5.0pp
<Percent value={{ kind: "bps", value: 25 }} /> // +25 bps
No default kind. The discriminator is required. Division by zero → n/a, never Infinity.
Precision by kind: relative/points 1dp · bps 0dp · rate 3dp.
5. Rule
The accounting rule lines. Two CSS declarations, four centuries of grammar.
<Rule variant="subtotal" /> // border-top: 1px solid --border
<Rule variant="total" /> // border-bottom: 3px double --border-strong
<Rule variant="section" /> // full-bleed 1px --border-subtle
Spans the numeric column only, not the label column. That's what makes it read as accounting rather than as a divider. In a multi-period statement it spans each period column independently, with the gutters unruled.
Never combined with a background fill on the same row.
Zero motion. Zero interactivity. Ten lines of code, and the single highest credibility-per-byte component in the library.
6. StatusPill
<StatusPill status="pending" /> // ◷ Pending amber, subtle bg
<StatusPill status="settled" /> // ✓ Settled positive
<StatusPill status="failed" /> // ✕ Failed negative
<StatusPill status="reconciled" />
<StatusPill status="needs-review" />
*-subtle background, * text, never a saturated fill. Icon + text always — the icon alone is not a status. Accessible name includes the entity: "Payment pending".
State change: 140ms crossfade with a 2px blur mask, so the pill morphs rather than swapping. One of the small places the blur trick earns its keep.
7. Sparkline
24–40px tall, no axes, no gridlines, no tooltip by default. --text-secondary stroke at 1.5px, or --positive/--negative by net direction, or neutral when polarity="neutral".
Optional endpoint dot and a single label at the last point.
Motion: stroke-dashoffset draw, 600ms --ease-out, once, on first paint or scroll-into-view ({ once: true }). Never on data change.
Touch: no scrub. A sparkline is an impression, not a chart. If a user needs values, they need MoneyChart.
8. Trend
Delta + Sparkline composed, with aligned baselines. The default "how is this doing" unit inside a card.
9. AsOf
<AsOf date={lastSync} /> // "2m ago" · title="6 Sep 2026, 09:12 UTC"
Relative under an hour, absolute beyond. Timezone always stated in the absolute form. Updates from a single shared interval — never one setInterval per instance — and pauses on tab hide and off-screen.
Every figure in this system is accompanied by one of these. A balance without an "as of" is a rumor.
10. Redacted
•••• at --text-tertiary, reveal on explicit action. Layout-stable: the masked form reserves the revealed width. Privacy-blur variant uses filter: blur(6px) + user-select: none, crossfading over 140ms so nothing reflows.
11. Reference
Transaction and account IDs. "zero" 1, "ss02" 1 so 0/O and 1/l/I are unambiguous — these get transcribed by humans into other systems. Click to copy; copies the raw value. Truncates in the middle (txn_8a2…f091), never at the end, because the tail is the discriminating part.
12. CurrencyBadge
ISO code, optionally with a flag. Flags are regional, not linguistic — EUR gets the EU flag, and any currency without a clean single-country mapping gets no flag rather than a wrong one. Code is always present; the flag is decoration.
13. EntityMark
Bank and merchant logos. Rounded-square container at radius 8, --surface-sunken background, ~20% inset, object-fit: contain, saturate(0.85) at row scale. Full fallback ladder — provided → resolved → deterministic monogram → category icon. Never a broken image. Spec in imagery.
169. Tooltip
A restatement of something already on screen. Never the only home of a fact.
<Tooltip content="30 Sep 2026, 09:12 UTC">
<time>2m ago</time>
</Tooltip>
<TooltipProvider delay={400}>{/* a toolbar, a table, an app */}</TooltipProvider>
interface TooltipProps {
/** Nullable on purpose: a truncated cell has one, a short cell does not. */
content: React.ReactNode;
children: React.ReactNode;
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
/** Overrides the provider's delay for this one trigger. */
delay?: number;
disabled?: boolean;
/** `block` for a child that fills its column — a truncating cell. */
display?: "inline" | "block";
triggerClassName?: string;
}
It renders its own trigger span and puts children inside it, rather than cloning the
caller's element. That is not a stylistic choice: a Server Component passes its JSX children
across the boundary as a lazy node, not an element — isValidElement is false and type is
undefined — so a render={children} clone renders undefined and takes the page down with it.
Rendering the child as a child works from either side of the boundary. The cost is one <span>,
and display="block" is how a truncating child keeps its width.
What may go in one. The unabbreviated figure behind $1.2M. The absolute timestamp behind
"2m ago". The untruncated merchant name behind an ellipsis. The rate and instant behind a ≈
conversion. The comparison basis behind a delta on a card too narrow to carry it inline. In every
case the tooltip restates — it never introduces.
What may not. Anything a user must read to act. Tooltips do not exist on touch, so a
sentence that only lives in one is a sentence a phone user never sees. If it is essential, inline
it; if it is additional, it belongs in an InfoTip, which opens on press.
Layer 3 — structure: radius 8, --surface-raised, hairline + --elevation-2, 12px caption,
max-width 264px, 8px from its anchor.
Motion: enter opacity 0→1, scale 0.97→1 over 140ms --ease-out; exit opacity over 100ms;
400ms delay before the first one. Once one is open, hovering a neighbour opens it instantly —
Base UI sets data-instant, and the transition duration must go to 0ms with it or the "instant"
tooltip still visibly fades. Wrap a dense surface in TooltipProvider to get that grouping.
Accessibility: the trigger keeps its own accessible name; the tooltip is aria-describedby.
It opens on keyboard focus, closes on Escape, and is never the target of a click.
170. InfoTip
The ⓘ. Additional context, reachable on every input.
<InfoTip about="Why this account needs reconnecting" label="This balance may be out of date">
Mercury revoked this connection's authorization. Last synced 21 Sep 2026, 09:12 UTC.
</InfoTip>
interface InfoTipProps {
/** An optional bolded first line. A term; omit it for a plain sentence. */
label?: React.ReactNode;
children: React.ReactNode;
/** The affordance's accessible name — "About available balance". */
about: string;
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
/** Replaces the ⓘ glyph — a warning triangle beside a broken state, say. */
trigger?: React.ReactNode;
}
A Popover, not a Tooltip, and that is the entire design. It opens on hover on a fine pointer,
on press everywhere, and on focus from the keyboard — so the sentence inside it is reachable on a
phone, which is what licenses putting a real sentence there at all. Escape and an outside press
close it. 16px glyph, 40px hit area, --text-tertiary at rest.
Use it to shorten an alert. "Reauthorization required — this balance may be out of date" in a
one-third-width card wraps to three lines and reads as a paragraph in a strip meant to read as a
state. "Reauthorization required" plus an ⓘ says the same thing in one line, and the explanation
is one press away instead of permanently in the way.
Never put an action, a link the user must follow, or the only copy of a number inside one.