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.
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 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.
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;
}
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);
}
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;
}
44×44px is the minimum. Targets smaller than this require invisible hit padding; targets inside dense tables get a whole-row hit area instead.
Icons-only hit area. Expand with invisible padding or reserve for secondary, low-frequency actions only.
< 44px · failsWCAG 2.5.5 minimum. The right size for most buttons, inputs, and row affordances.
44px · AA minComfortable, hit-able in motion. Use for primary actions and phone-first surfaces.
56px · comfortableMeaning carried only by hue is invisible to ~8% of users with color-vision differences. Pair every color signal with shape, text, or position.
Could be success, warning, danger — the dot alone can't say.
● Failed · retry → Hue supports the text, doesn't replace it.
Prefer real HTML elements. ARIA is a last resort — only reach for it when no native element fits. One live-region example:
<!-- 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>
The short, non-negotiable list.
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.
outline: 0 without a replacement is a bug. Always swap in a visible ring — the token system has three styles for a reason.
Every button without visible text gets aria-label. Screen readers are our users too.
Modals should hold focus inside until dismissed; nothing else should. Escape always works.
All animation defers to prefers-reduced-motion — instant transitions, no looping decoration. See Motion.
Ever. Sound is a user-initiated thing; notification bleeps have a setting.