Skip to content

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

tertiary text — decorative

Soft ink — body fails at 3.4:1.

--fg-softfails AA
tertiary text — AA-safe

Soft-AA ink — same role, body passes at 4.5:1.

--fg-soft-aaAA 4.6:1
status — Approved · 200 OK

Dot uses light · text uses deep — different roles.

--status-ok-textAAA 7.2:1
status — Failed · 500 ERR

Same recipe, error palette.

--status-err-textAA 4.8:1

Soft text — the decorative / AA-compliant pair

TokenRatio on --bgUse
--fg-soft3.4:1decorative — captions over chrome, ambient annotation. Fails AA at body.
--fg-soft-aa4.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.

Run bun run --filter @matter/components test:axe before opening a PR that adds an interactive component.
Don't ship a custom focus indicator. The canonical ring is the contract.

On this page