Design System
Foundations · Accessibility

Accessibility.

A11y is not a feature to bolt on — it's a contract the tokens sign for us. Every role alias has been tuned to clear WCAG 2.2 AA, focus rings are visible by default, and targets hit at least 44×44px. This page documents the contract, so you can ship against it.

Contrast matrix

Measured against #FFFFFF (light) and #0B0A14 (dark). AA large = 3.0:1 · AA normal = 4.5:1 · AAA = 7.0:1.

Token Sample On white On dark Use for
iris-500 Primary 4.82 AA 3.90 large Primary button, active state
iris-700 Link 7.64 AAA 2.46 fail Inline links on light bg
iris-300 Primary 2.22 fail 8.45 AAA Dark-mode primary, links on dark
coral-500 Secondary 3.21 large 5.84 AA Large UI only; use coral-700 for small text
teal-500 Accent 2.55 fail 7.36 AAA Accent fills; never for text on white
ink-muted Body muted 7.56 AAA Secondary body copy, hints
ink-subtle Meta 4.66 AA Captions, timestamps, meta
success Saved 3.07 large 6.11 AA Success fill; use text on surface for < 18px
warning Warning 2.05 fail Always pair with dark ink (#451A03)
danger Delete 4.84 AA 3.88 large Destructive actions, errors

Focus

Focus is a promise — the user must always know where the keyboard is. Three styles cover every component: offset ring for solid surfaces, inner ring + border for inputs, and underline + outline for inline text.

Solid surfaces

Buttons, cards, chips. 3px iris-300 outline, 2px offset from the element. Never clips.

:focus-visible{
  outline: 3px solid var(--iris-300);
  outline-offset: 2px;
}

Form inputs

Border shifts to iris-500, 4px iris-100 ring blooms inside. Works with flat rows and grouped inputs.

:focus-visible{
  border-color: var(--iris-500);
  box-shadow: 0 0 0 4px var(--iris-100);
}

Inline text

Links are underlined by default. On focus, an outline bracket the text so the focus is visible even on busy backgrounds.

a:focus-visible{
  outline: 2px solid var(--iris-500);
  outline-offset: 4px;
  border-radius: 3px;
}

Touch targets

44×44px is the minimum. Targets smaller than this require invisible hit padding; targets inside dense tables get a whole-row hit area instead.

28

28 · too small

Icons-only hit area. Expand with invisible padding or reserve for secondary, low-frequency actions only.

< 44px · fails
44

44 · minimum

WCAG 2.5.5 minimum. The right size for most buttons, inputs, and row affordances.

44px · AA min
56

56 · recommended

Comfortable, hit-able in motion. Use for primary actions and phone-first surfaces.

56px · comfortable

Never color alone

Meaning carried only by hue is invisible to ~8% of users with color-vision differences. Pair every color signal with shape, text, or position.

Normal vision
green · amber · red · blue
Protanopia
red–green weak · success ≈ danger
Deuteranopia
most common type
Tritanopia
blue–yellow weak · rare
Don't

Color-only status dot

Could be success, warning, danger — the dot alone can't say.

Do

Dot + label + icon

● Failed · retry → Hue supports the text, doesn't replace it.

Semantics & ARIA

Prefer real HTML elements. ARIA is a last resort — only reach for it when no native element fits. One live-region example:

Workspace saved
<!-- Polite live region for async confirmations -->
<div role="status"
     aria-live="polite"
     aria-atomic="true">
  Workspace saved
</div>

<!-- Icons-only buttons: always labeled -->
<button aria-label="Delete workspace">
  <svg aria-hidden="true">…</svg>
</button>

Rules

The short, non-negotiable list.

Do

Test with keyboard first

Tab through every screen before you ship. If you can't reach it with keys or the focus is invisible, the surface isn't done.

Don't

Don't remove focus styles

outline: 0 without a replacement is a bug. Always swap in a visible ring — the token system has three styles for a reason.

Do

Label icon-only controls

Every button without visible text gets aria-label. Screen readers are our users too.

Don't

Don't trap focus

Modals should hold focus inside until dismissed; nothing else should. Escape always works.

Do

Ship reduced-motion

All animation defers to prefers-reduced-motion — instant transitions, no looping decoration. See Motion.

Don't

Don't auto-play sound

Ever. Sound is a user-initiated thing; notification bleeps have a setting.