Accessibility — AA contract
The contract
- Body text ≥ 4.5:1 (WCAG 2.1 AA at body size).
- UI and large text ≥ 3:1 (large = ≥ 18 pt regular or ≥ 14 pt bold).
- Decorative tokens are exempt — gradient stops, glass alphas, bloom palettes are not subject to contrast.
- Sibling AA aliases exist for callsites that need to flip from a decorative variant to an AA-compliant one.
Rendered — pass/fail samples
Soft ink — body fails at 3.4:1.
Soft-AA ink — same role, body passes at 4.5:1.
Dot uses light · text uses deep — different roles.
Same recipe, error palette.
Soft text — the decorative / AA-compliant pair
| Token | Ratio on --bg | Use |
|---|---|---|
--fg-soft | 3.4:1 | decorative — captions over chrome, ambient annotation. Fails AA at body. |
--fg-soft-aa | 4.5:1+ | AA-compliant sibling — body, table secondaries, anywhere a reader will parse the text. |
Same visual role, two contrast levels. Pick the one that matches the surface.
Status colors — two roles
Status colors play two roles:
- Lighter variant (
--status-ok-dot) — decorative, exempt from contrast, used for the dot. - Deeper variant (
--status-ok-text) — the only one allowed on text.
They never need to match because they play different roles.
Forced colors
The system imports @matter/tokens/css/forced-colors at the top of every app's global stylesheet. Windows High Contrast / Forced Colors users see the right system mappings — no per-component overrides.
Reduced motion
Every m-* keyframe respects prefers-reduced-motion: reduce:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}Bloom keyframes (motion-blur stripes) stop completely under this query.
Focus
Every focusable surface inherits the canonical peach ring — see Focus.
Keyboard
Every interactive component is keyboard-traversable. The Radix-backed primitives in @matter/components ship with aria-* and roving focus baked in. Hand-authored components must include a keyboard transcript on their MDX page.
bun run --filter @matter/components test:axe before opening a PR that adds an interactive component.