Skip to content

Browser design system

Agent WebUI builds every browser surface from one set of tokens, primitives and interaction rules rather than inventing new ones per view. This page documents the current tokens sourced from src/index.css and the src/components/ui/ primitives that consume them, including the contrast targets a new surface must meet. A token audit test (src/__tests__/design-tokens.test.ts) checks this page's values against the live stylesheet and computes the real contrast ratio for every pairing below, so this document cannot silently drift from the source of truth.

Color tokens

Values are OKLCH; :root defines the light theme and .dark overrides it (a .glass theme layers translucency on the dark palette for optional use). Normal text targets 4.5:1 contrast against its background; large text, icons and meaningful control boundaries or focus indicators target 3:1 — per WCAG 2.2 success criteria 1.4.3 and 1.4.11. Color is never the only signal for a state; see State tokens.

Role (pairing) Light Dark Contrast target
Foreground on Background oklch(0.145 0.012 265) on oklch(0.988 0.002 280) oklch(0.96 0.005 260) on oklch(0.13 0.02 260) 4.5:1
Primary-foreground on Primary oklch(0.98 0.005 260) on oklch(0.52 0.16 260) oklch(0.10 0.015 260) on oklch(0.62 0.16 260) 4.5:1
Secondary-foreground on Secondary oklch(0.22 0.01 280) on oklch(0.96 0.004 280) oklch(0.96 0.005 260) on oklch(0.22 0.015 260) 4.5:1
Muted-foreground on Background oklch(0.50 0.02 280) on oklch(0.988 0.002 280) oklch(0.64 0.02 260) on oklch(0.13 0.02 260) 4.5:1
Accent-foreground on Accent oklch(0.22 0.01 280) on oklch(0.95 0.04 260) oklch(0.96 0.005 260) on oklch(0.20 0.04 260) 4.5:1
Destructive text on Background oklch(0.58 0.24 27) oklch(0.70 0.19 22) 4.5:1
Ring (focus indicator) on Background oklch(0.52 0.16 260) oklch(0.62 0.16 260) 3:1

Typography

The body font is Inter, loaded in src/index.css with a system-UI fallback stack (system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif) so text never disappears before the webfont loads. index.css adds one custom size token, --text-tiny (0.625rem / 10px), for the smallest captions and badges; every other size comes from Tailwind's default type scale so it stays consistent with the rest of the ecosystem's tooling.

Spacing and density

--radius (0.625rem) is the one spacing primitive in index.css; -sm, -md, -lg and -xl are derived from it (calc(var(--radius) ± Npx)) so corner rounding stays proportional across control sizes. Interactive density follows the existing components rather than a separate scale: header and launcher controls (ChatHeader, the chat FAB) use a 40px (size-10) touch target, compact inline controls (Atlas filter clauses) use a 32px (size-8/ h-8) target, and no actionable control drops below 24px — consistent with WCAG 2.2 2.5.8 Target Size (Minimum). Required actions wrap onto additional lines at narrow widths instead of being clipped or hidden (see FilterBar's flex-wrap/min-w-0 layout).

Focus

src/index.css defines one global focus style:

:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

Every interactive primitive in src/components/ui/ inherits this outline by being a real button/a/form control, so a new component gets a visible keyboard focus indicator for free as long as it reuses those primitives instead of a bare div. Components that need a stronger in-context ring (for example ChatHeader's icon buttons) layer Tailwind's focus-visible:ring-2 focus-visible:ring-ring on the same --ring token rather than inventing a new color.

State tokens (status language)

Loading, empty, stale, denied, error, success and pending states are each written out in text; color reinforces but never carries the distinction alone (DS-05). src/components/ui/status-message.tsx (StatusMessage) is the shared primitive for exactly this seven-state vocabulary, covering the one case BlockedState.tsx does not (whole-panel/integration availability) — the outcome of a single item, field or operation. Each state's label is fixture-tested in src/components/ui/__tests__/status-message.test.tsx to confirm every state renders text distinct from every other state. The existing convention, used by StatusMessage, BlockedState.tsx and the view loading/error fallbacks:

State Color cue Required text
Loading muted-foreground + spinner icon An explicit "Loading…" (or equivalent) label stays in the DOM even when the spinner's motion is removed
Empty muted-foreground An explicit "No results" / "Nothing here yet" message, not a blank area
Stale amber/warning utility The word "stale" (or equivalent) next to the data
Denied destructive An explicit "denied" / "not permitted" message, not just a red border
Error destructive The actual error message text, associated to its field via aria-describedby where it is a form error
Success emerald/success utility An explicit "saved" / "succeeded" message
Pending muted/amber The word "pending" (or equivalent) next to the item

Reduced motion

src/index.css disables nonessential animation and transition motion for every element when the user has requested it, while leaving all text, icons and layout fully visible:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Components that need an explicit reduced-motion alternative beyond this global floor (for example the chat drawer's open/close transition and its decorative ping/pulse) additionally use Tailwind's motion-reduce: variant, documented alongside the components it touches in specs/browser-design-system/interaction-contract.md. Every gesture-driven action in the reviewed journeys also has a plain click or keyboard equivalent — see the same interaction contract.

Reuse

Build new surfaces from src/components/ui/ and these tokens; do not add a parallel button, dialog, filter, tooltip, renderer or color system. A pattern borrowed from an outside source is paraphrased in original wording only after its license is checked, and never pulled in as a live or copied dependency.