design-system

DAX Design System

docs/design-system/README.mdtype: design-system-referenceupdated: 2026-05-04

DAX Design System

Practical reference for the CSS primitives that power user, admin, and merchant surfaces. Source of truth lives in code:

app/styles/tokens.css      ← all CSS custom properties
app/styles/typography.css  ← font roles + utilities + coin lockup
app/styles/motion.css      ← keyframes + reduced-motion guard
app/globals.css            ← component primitives (.b-* / .dax-*) + reset + focus rules

Two palettes, one component vocabulary. Audience determines the palette via [data-shell="user|admin|merchant"] on the layout root — there is no runtime theme toggle.

Shell Palette Surfaces When
user, admin (default) Bright sage oklch(0.94 .. 1) light End-user wallet + admin console
merchant Deep slate oklch(0.19 .. 0.27) dark Merchant payouts / invoices / API console

Tokens

All tokens are CSS custom properties scoped per-shell. Consume tokens — never inline OKLCH literals in component CSS.

Surfaces

Token Role
--bg Page background
--bg-elev Panel / card on top of --bg
--bg-sunk Inset surface (input rows, action chips, sub-cards)

Ink

Token Role Contrast on --bg
--ink Primary text AAA
--ink-2 Secondary text, table cells, sidebar nav idle AA
--ink-3 Muted captions, table headers, time stamps AA (lifted on merchant from 0.58 → 0.62 to clear small-text AA)

Status

Each status color comes with a -soft companion. Use --pos etc. for foreground (text, icons, lines), -soft for backgrounds (pills, banner fills, row tints).

Token -soft When
--pos --pos-soft Successful payment, paid invoice, completed batch
--neg --neg-soft Errors, blocked rows, void / due / failed
--warn --warn-soft Pending, opened, executing, broadcast
--info --info-soft Validated, ready, neutral system info

Borders

Token Role
--rule Default borders (panels, lines, table headers)
--rule-2 Subtle dividers (table inner rows)

Accent

Token Role
--accent Brand accent — focus rings, primary CTA on merchant, ticker prompt
--accent-soft Active states (chip on, asset selected, accent pill bg)
--accent-ink Foreground that sits on --accent-soft

Type families

Token Stack
--font-sans Inter → system sans
--font-mono JetBrains Mono → SF Mono → Menlo
--font-serif Aliased to --font-sans (Newsreader retired)

Type scale

--text-2xs 10px   — kbd hints, table sublabels
--text-xs  11px   — muted captions
--text-sm  12px   — secondary body, mono data
--text-md  13px   — tables, default body
--text-base 14px  — form inputs, primary body
--text-lg  16px   — sidebar wallet fiat
--text-xl  22px   — panel headers
--text-2xl 28px   — section heroes
--text-3xl 44px   — balance totals (mono, tnum)

Line-height tokens: --leading-tight 1.15 · --leading-snug 1.35 · --leading-base 1.5 Tracking tokens: --tracking-tight -0.02em · --tracking-snug -0.01em · --tracking-loose 0.04em

Radius

Token Value Use
--radius-sharp 2px Sort buttons, micro chips
--radius-sm 3px Form lines, table inner cells, default chip
--radius-md 8px Secondary buttons, input groups
--radius-lg 14px Panels, cards, login surfaces
--radius-pill 999px Sidebar nav, primary buttons, status pills

Spacing (4-step grid)

--space-1 4 · --space-2 8 · --space-3 12 · --space-4 16 · --space-5 20 · --space-6 24 · --space-8 32 · --space-10 40

Motion

Token Value Use
--motion-fast 120ms Hover state shifts, chip toggles
--motion-base 200ms Default UI transitions
--motion-slow 320ms Page-level fade-in, drawer open
--ease-out cubic-bezier(0.2, 0, 0, 1) Default for entering/state changes
--ease-in cubic-bezier(0.4, 0, 1, 1) Exit/dismiss transitions

Typography

Three layers — pick the layer that matches your intent.

Utilities

Class Effect
.mono JetBrains Mono + tabular numbers
.tnum Tabular numbers only (keep current font)
.serif Currently aliased to sans (Newsreader retired)

Semantic roles

Apply by intent, not by appearance.

Class Family Size When
.t-display mono --text-3xl Balance totals, hero numbers
.t-headline sans --text-xl Panel headers (▸ surface.name)
.t-title sans --text-md 500 Card titles, list items
.t-body sans --text-md 400 Default body copy
.t-data mono --text-sm Tables, KV pairs, terminal logs
.t-label mono --text-2xs uppercase Form labels, section captions

Decorative

Class Use
.coin Single-letter colored monogram disc — pair with `data-sym="BTC

Component primitives

All primitives live in app/globals.css. The list below is non-exhaustive — app/globals.css is the canonical source. Use these classes; do not hand-roll Tailwind utility salads on merchant/admin surfaces.

Layout shell — .b-shell, .b-side, .b-topbar

<div className="dir-b">
  <div className="b-shell">
    <aside className="b-side">…sidebar…</aside>
    <div className="b-topbar">…topbar…</div>
    <main>…page content (typically wrapped in `.b-grid` or `.b-flow`)…</main>
  </div>
</div>
  • .b-side is a 240px fixed left rail; collapses to off-canvas drawer ≤900px (controlled by data-side-open on .b-shell).
  • .b-topbar is a slim 14×22 row aligned with sidebar top; holds the icon strip and logout button.

Panel — .b-panel

The canonical content surface. Always paired with .b-panel-h + .b-panel-body.

<section className="b-panel" style={{ gridColumn: '1 / -1' }}>
  <div className="b-panel-h mono">
    <span>▸ surface.name</span>
    <span className="b-panel-sub">12 rows · updated 2m ago</span>
  </div>
  <div className="b-panel-body">…content…</div>
</section>

Tables — .b-table

<table className="b-table mono">
  <thead><tr><th>#</th><th>address</th><th className="r">amount</th></tr></thead>
  <tbody>
    <tr className="clickable">
      <td>…</td>
      <td>…</td>
      <td className="r tnum">…</td>
    </tr>
  </tbody>
</table>

States: tr:hover--bg-sunk, tr.on--accent-soft, tr.clickable cursor: pointer.

Status pills — .b-inv-status.s-<status>

Class Color family Used for
.s-draft, .s-expired, .s-dead muted gray Inactive
.s-sent, .s-detected accent Soft activity
.s-opened, .s-open, .s-pending, .s-broadcast, .s-executing, .s-queued, .s-underpaid warn (amber) In-flight
.s-validated, .s-ready, .s-overpaid info (blue) Pre-flight / surplus
.s-paid, .s-confirmed, .s-completed, .s-delivered pos (green) Success
.s-due, .s-overdue, .s-void, .s-hold, .s-failed, .s-blocked, .s-invalid_address, .s-invalid_amount, .s-insufficient_balance neg (red) Error / blocking
.s-partial_success orange Mixed outcome

Form lines — .b-line

<label className="b-line">
  <span className="b-lbl">amount:</span>
  <input type="text" />
</label>

States: :focus-within → border switches to --accent. For invalid input, add aria-invalid="true" on the wrapper (<label>) — wire styling via .b-line[aria-invalid="true"] if you need a visible error tint.

Buttons — .b-btn, .b-btn.primary, .b-btn.ghost

<div className="b-btn-row">
  <button className="b-btn ghost">cancel</button>
  <button className="b-btn primary">confirm ⏎</button>
</div>

Chips — .b-chip

<div className="b-chips">
  <button className="b-chip on">all</button>
  <button className="b-chip">paid</button>
  <button className="b-chip">due</button>
</div>

Filter grid — <FilterGrid> / <FilterRow> / <FilterForm>

Use the React components in components/filters/filter-grid.tsx, not raw .dax-filter-* classes. They keep status / created / search rows aligned across pages.


Accessibility

Focus rings

A single global rule in app/globals.css covers all interactive primitives (a, button, [role="button"], form controls, [tabindex]). The ring is 2px solid var(--accent) with a 2px offset.

Don't override or remove this in component CSS. If a primitive needs a custom ring, replace via :focus-visible only (not :focus).

Reduced motion

app/styles/motion.css ends with a @media (prefers-reduced-motion: reduce) block that:

  1. Caps all animation/transition durations at 0.01ms globally.
  2. Keeps the braille loading spinner running (slows it to 1s steps) — it communicates ongoing work and silencing it would create false-success confusion.
  3. Replaces shimmer skeletons with a flat --bg-sunk block.

Contrast

Run color pairs through a contrast checker before adding a new token. Targets:

  • Body text (≥14px): 4.5:1
  • Large text (≥18px or ≥14px bold): 3:1
  • UI glyphs (icons, dividers): 3:1

Merchant --ink-3 was lifted from oklch(0.58 …) to oklch(0.62 …) to clear AA on .b-panel-sub, .muted, and table headers.

Keyboard navigation

  • All interactive elements must be reachable via Tab in visual order.
  • Use <button> for actions, <a> for navigation. Don't put click handlers on bare <div>s.
  • Modal dismiss must be wired to ESC. The login flow uses <form> so Enter submits naturally.
  • Tables: rows with .clickable should announce as buttons or wrap an <a> — not a click handler on the <tr>.

Pre-delivery checklist

Before merging UI changes, verify:

  • No new component-local OKLCH literals (audit git diff for oklch()
  • No font-size: / font-family: in components — use .t-* or token
  • No new keyframes outside app/styles/motion.css
  • Hover/active/focus states all visually distinct
  • Both shells exercised (light + dark) — check [data-shell] toggle in DevTools
  • Touch targets ≥44px on phone breakpoint
  • Tested at 375 / 768 / 1024 / 1440
  • No console.log left over (pnpm lint will catch)
  • pnpm typecheck clean
  • pnpm test clean

Migration notes

The system was reorganized in May 2026. If you're updating older code that pre-dates this:

  • font-family: var(--font-newsreader) → use .t-display or var(--font-serif) (which now aliases to sans).
  • Inline oklch(0.96 0.07 28) for error backgrounds → var(--neg-soft).
  • Inline oklch(0.95 0.05 155) for success backgrounds → var(--pos-soft).
  • Inline oklch(0.95 0.06 80) for warning backgrounds → var(--warn-soft).
  • Inline oklch(0.95 0.05 255) for info backgrounds → var(--info-soft).
  • Hard-coded border-radius: 14px on a panel → var(--radius-lg).
  • Hard-coded transition: … .14svar(--motion-fast) var(--ease-out).

Migrating one component per PR is fine — old hex values still resolve to the same colors, so behavior is preserved.