Accessibility matrix
Matter's accessibility contract is AA, always. Every component renders correctly under six conditions: keyboard navigation, screen readers, focus visibility, color contrast, reduced motion, and forced colors. The matrix below is one row per component, one column per condition.
This page complements the accessibility-contract page (which states the global rules). The matrix is the per-component sweep — the page you check before shipping a consumer that uses these components.
Reading the matrix
Each component's a11y: frontmatter block describes how it handles each condition. The matrix below excerpts the key claims; the per-component MDX page has the full prose.
If a component fails one of these checks in the wild, file a regression issue tagged design-system:a11y. The maintainer rolls back within 24h.
Domain cards
| Component | Keyboard | Screen reader | Focus | Reduced motion | Forced colors |
|---|---|---|---|---|---|
| BoardConsentCard | Send-reminder & View-document; signer rows non-interactive | Title, "Awaiting signatures", each signer as "name, role, signed/pending" | Peach ring on footer buttons | No motion in steady state | Avatar → Highlight; pill dot → ButtonText |
| FilingStatusCard | Non-interactive card | Title, status label, jurisdiction, due date, each item | N/A internally; wrapper carries focus | Static | Status pill bg → Highlight; check fill → ButtonText |
| OptionGrantCard | Approve-grant; Adjust; Open-in-Equity | Title + grant ID, recipient + role, shares/strike/plan, vesting summary | Peach ring on footer | Hover lift → opacity | Vesting bar → Highlight; cliff marker → ButtonText |
| CapTableSnapshotCard | Open-Cap-Table; Export-to-Excel | Donut as role=img with aria-label; holder rows as "name, role, %" | Peach ring on footer | No motion (no mount animation) | Donut segments → alternating Highlight/Canvas |
| PlanCard | Non-interactive | Title + x/n progress, each item as "done/in-progress/pending" | N/A | No motion in steady state | Done fill → ButtonText; active dot → Highlight |
| SignatureStack | Send-for-signature; Edit | Eyebrow, package name, status, each doc as "title, signers, pages, size" | Peach ring on footer | Hover lift → opacity | Document thumb → Canvas; avatars → Highlight |
| Timeline (namespace) | Search input + FilterSelect + KindToggleRow chips | Each event "kind, actor, action, source, timestamp" | Peach ring on filters | KindToggleRow transition → opacity | KindDot → ButtonText; chip bg → Highlight |
Composer surfaces
| Component | Keyboard | Screen reader | Focus | Reduced motion | Forced colors |
|---|---|---|---|---|---|
| Composer | Real <textarea>; Enter submits, Shift+Enter newlines | Placeholder = accessible name; send announces "Send" | Peach ring on textarea + send | Backdrop refraction stays; chromatic-aberration drift stops | Glass → Canvas + ButtonBorder |
| ComposerChip | Real <button>; Enter/Space activate | Icon decorative; children = accessible name | Peach ring | Hover lift → opacity | Background → ButtonFace; foreground → ButtonText |
| ComposerLensDefs | None (decorative SVG) | aria-hidden | N/A | Static | No effect (backdrop-filter unsupported in FC) |
| InlineToolCall | Non-interactive pill | "METHOD path, status · latency" | N/A | Pending-state dot pulse → static | Dot → ButtonText |
| SlidingPill | Real <button> per item; Tab between | "label, button"; consumer wires aria-pressed for toggle | Peach ring per item | Indicator slide → instant | Indicator → Highlight; active text → HighlightText |
Surfaces
| Component | Keyboard | Screen reader | Focus | Reduced motion | Forced colors |
|---|---|---|---|---|---|
| Card | Non-interactive container | Children in DOM order | No internal focus | No motion | Background → Canvas; border → ButtonBorder |
| Sheet | Escape closes; NO automatic focus trap (use Dialog for trap) | role=dialog, aria-modal; consumer supplies aria-labelledby | Consumer responsibility | Scrim + card transition → instant | Scrim → semi-Canvas; card → Canvas + ButtonBorder |
| Glass | Non-interactive | Children in DOM order | No internal focus | No motion | Glass collapses to Canvas + ButtonBorder; ::before disappears |
| BloomArt | None | aria-hidden | N/A | Static | Gradients flatten to a single Canvas fill |
| BloomBackdrop | None | aria-hidden | N/A | Static | Bloom + veil disappear |
| Section | None on the section | Region — pair with aria-labelledby for named regions | N/A | Static | Bloom backdrop disappears |
Primitives
| Component | Keyboard | Screen reader | Focus | Reduced motion | Forced colors |
|---|---|---|---|---|---|
| Button | Real <button>; Enter/Space activate | Children = accessible name; state announced | Peach ring | Hover lift → opacity | Variants → ButtonFace/ButtonText |
| Pill | Non-interactive | Verbatim text | N/A | N/A | Tone bg → Highlight; fg → HighlightText |
| Eyebrow | Non-interactive | Text before heading | N/A | N/A | Foreground → system foreground |
| AppBreadcrumb | Interactive crumbs reachable via Tab; current non-focusable | nav + ol; last crumb aria-current=page | Peach ring on interactive crumbs | Static | Separators → ButtonBorder; current → HighlightText |
| EndpointBadge | Non-interactive | "METHOD path" or "METHOD" verb-only | N/A | Static | Method bg → Highlight; fg → HighlightText |
Headings & code
| Component | Keyboard | Screen reader | Focus | Reduced motion | Forced colors |
|---|---|---|---|---|---|
| DisplayHeading | Non-interactive | Announces as heading at as level | scroll-margin-top for anchored navigation | Static | System foreground; weight + tracking preserved |
| OrgMark | Non-interactive swatch | aria-label="Organisation {name}" | Wrapper carries focus | Static | Accent → Highlight; initials → HighlightText |
| CodeBlock | Non-interactive | Body announces verbatim; figure mode allows figcaption | Triple-click selects code | Static | Tok colors → system foreground |
| CodeCard | Non-interactive | Head "verb path time"; body verbatim | N/A | Static | Verb chip → Highlight; body → system foreground |
Brand wrappers
| Component | Keyboard | Screen reader | Focus | Reduced motion | Forced colors |
|---|---|---|---|---|---|
| MatterButton | Real <button>; Enter/Space activate; asChild delegates | Children + state | Peach ring | Hover lift → opacity | Variants → ButtonFace/ButtonText; outline → ButtonBorder |
| MatterEyebrow | Non-interactive | Text before heading | N/A | Static | System foreground; tracking + uppercase preserved |
| BetaPill | Wrapper link supplies keyboard | "chip text, body text" | Wrapper link | Chevron translate → opacity | Chip bg → Highlight; chevron → ButtonText |
| MonoChip | Non-interactive | Verbatim | N/A | Static | Tone bg → Highlight; fg → HighlightText |
| StatusPill | Non-interactive | Avatar decorative; text plain; wrap in output/aria-live for live updates | N/A | Shimmer → static fill | Shimmer → system foreground |
| VerbPill | Non-interactive | Verb verbatim | N/A | Static | Background → Highlight; foreground → HighlightText |
The cross-cutting rules
Every component above respects the same six global rules:
- Keyboard reachable. Tab order matches visual order. Enter / Space activate. Escape closes. Arrow keys navigate where applicable.
- Screen-reader correct. Semantic HTML first; ARIA only where semantics don't suffice. Live regions for status updates.
- Focus visible.
:focus-visible(not:focus) ring on every interactive element. Canonical peach ring via--focus-ring. - AA contrast. Every foreground-on-background pair meets WCAG AA (4.5:1 normal, 3:1 large). Verify with the color foundation contrast samples.
- Reduced motion.
@media (prefers-reduced-motion: reduce)collapses animation to opacity transitions. Never disables interaction. - Forced colors. Every component remains identifiable when the system overrides colors. Shape, spacing, semantic affordance survive.
Automated gates
| Gate | Where | Runs |
|---|---|---|
| Token usage (no raw hex in MDX) | check-design-drift.ts invariant 4 | CI on every PR |
| Component coverage (every export has MDX) | check-design-drift.ts invariants 1 + 2 | CI on every PR |
| Token / TS↔CSS parity | packages/brand/test/tokens-sync.test.ts | CI on every PR |
| Per-component a11y prose presence | check-design-drift.ts invariant 7 | CI on every PR |
| Axe-core sweep on every rendered page | axe-sweep.ts | Run manually via bun run --filter design a11y |
| Per-viewport visual regression | Chromatic (when wired) | Visual diff on PRs |
The matrix above is the current coverage.
Running the axe sweep locally
# One-time setup
bun add -D @playwright/test @axe-core/playwright
bunx playwright install chromium
# Boot the design site in one terminal
bun run --filter design dev
# Run the sweep in another
bun run --filter design a11yThe script visits 24 canonical URLs (section roots + representative deep pages + client-island components), runs axe-core against each with the WCAG 2.0 AA + 2.1 AA + best-practice tag set, writes per-page violations to apps/design/.generated/axe-sweep.json, and exits non-zero if any violation lands.
Run it after every significant content change. The script uses dynamic imports for both Playwright deps so a clean checkout produces a helpful "run bun add -D first" message instead of an opaque resolve error.
When to file a regression
Open an issue tagged design-system:a11y when:
- A component fails one of the six conditions in production.
- A screen reader announces a component in a way that surprises a user.
- A focus state is missing, hidden, or inconsistent.
- A keyboard interaction is missing or doesn't match the documented contract.
The maintainer triages within 24h and rolls back within 24h if the regression is severe.
Reference
- WAI-ARIA Authoring Practices — patterns for complex widgets.
- Web Content Accessibility Guidelines 2.2 (WCAG) — the canonical accessibility standard.
- Web Interface Guidelines — the 69-rule reference Matter uses for surface-level accessibility checks.