Skip to content

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

ComponentKeyboardScreen readerFocusReduced motionForced colors
BoardConsentCardSend-reminder & View-document; signer rows non-interactiveTitle, "Awaiting signatures", each signer as "name, role, signed/pending"Peach ring on footer buttonsNo motion in steady stateAvatar → Highlight; pill dot → ButtonText
FilingStatusCardNon-interactive cardTitle, status label, jurisdiction, due date, each itemN/A internally; wrapper carries focusStaticStatus pill bg → Highlight; check fill → ButtonText
OptionGrantCardApprove-grant; Adjust; Open-in-EquityTitle + grant ID, recipient + role, shares/strike/plan, vesting summaryPeach ring on footerHover lift → opacityVesting bar → Highlight; cliff marker → ButtonText
CapTableSnapshotCardOpen-Cap-Table; Export-to-ExcelDonut as role=img with aria-label; holder rows as "name, role, %"Peach ring on footerNo motion (no mount animation)Donut segments → alternating Highlight/Canvas
PlanCardNon-interactiveTitle + x/n progress, each item as "done/in-progress/pending"N/ANo motion in steady stateDone fill → ButtonText; active dot → Highlight
SignatureStackSend-for-signature; EditEyebrow, package name, status, each doc as "title, signers, pages, size"Peach ring on footerHover lift → opacityDocument thumb → Canvas; avatars → Highlight
Timeline (namespace)Search input + FilterSelect + KindToggleRow chipsEach event "kind, actor, action, source, timestamp"Peach ring on filtersKindToggleRow transition → opacityKindDot → ButtonText; chip bg → Highlight

Composer surfaces

ComponentKeyboardScreen readerFocusReduced motionForced colors
ComposerReal <textarea>; Enter submits, Shift+Enter newlinesPlaceholder = accessible name; send announces "Send"Peach ring on textarea + sendBackdrop refraction stays; chromatic-aberration drift stopsGlass → Canvas + ButtonBorder
ComposerChipReal <button>; Enter/Space activateIcon decorative; children = accessible namePeach ringHover lift → opacityBackground → ButtonFace; foreground → ButtonText
ComposerLensDefsNone (decorative SVG)aria-hiddenN/AStaticNo effect (backdrop-filter unsupported in FC)
InlineToolCallNon-interactive pill"METHOD path, status · latency"N/APending-state dot pulse → staticDot → ButtonText
SlidingPillReal <button> per item; Tab between"label, button"; consumer wires aria-pressed for togglePeach ring per itemIndicator slide → instantIndicator → Highlight; active text → HighlightText

Surfaces

ComponentKeyboardScreen readerFocusReduced motionForced colors
CardNon-interactive containerChildren in DOM orderNo internal focusNo motionBackground → Canvas; border → ButtonBorder
SheetEscape closes; NO automatic focus trap (use Dialog for trap)role=dialog, aria-modal; consumer supplies aria-labelledbyConsumer responsibilityScrim + card transition → instantScrim → semi-Canvas; card → Canvas + ButtonBorder
GlassNon-interactiveChildren in DOM orderNo internal focusNo motionGlass collapses to Canvas + ButtonBorder; ::before disappears
BloomArtNonearia-hiddenN/AStaticGradients flatten to a single Canvas fill
BloomBackdropNonearia-hiddenN/AStaticBloom + veil disappear
SectionNone on the sectionRegion — pair with aria-labelledby for named regionsN/AStaticBloom backdrop disappears

Primitives

ComponentKeyboardScreen readerFocusReduced motionForced colors
ButtonReal <button>; Enter/Space activateChildren = accessible name; state announcedPeach ringHover lift → opacityVariants → ButtonFace/ButtonText
PillNon-interactiveVerbatim textN/AN/ATone bg → Highlight; fg → HighlightText
EyebrowNon-interactiveText before headingN/AN/AForeground → system foreground
AppBreadcrumbInteractive crumbs reachable via Tab; current non-focusablenav + ol; last crumb aria-current=pagePeach ring on interactive crumbsStaticSeparators → ButtonBorder; current → HighlightText
EndpointBadgeNon-interactive"METHOD path" or "METHOD" verb-onlyN/AStaticMethod bg → Highlight; fg → HighlightText

Headings & code

ComponentKeyboardScreen readerFocusReduced motionForced colors
DisplayHeadingNon-interactiveAnnounces as heading at as levelscroll-margin-top for anchored navigationStaticSystem foreground; weight + tracking preserved
OrgMarkNon-interactive swatcharia-label="Organisation {name}"Wrapper carries focusStaticAccent → Highlight; initials → HighlightText
CodeBlockNon-interactiveBody announces verbatim; figure mode allows figcaptionTriple-click selects codeStaticTok colors → system foreground
CodeCardNon-interactiveHead "verb path time"; body verbatimN/AStaticVerb chip → Highlight; body → system foreground

Brand wrappers

ComponentKeyboardScreen readerFocusReduced motionForced colors
MatterButtonReal <button>; Enter/Space activate; asChild delegatesChildren + statePeach ringHover lift → opacityVariants → ButtonFace/ButtonText; outline → ButtonBorder
MatterEyebrowNon-interactiveText before headingN/AStaticSystem foreground; tracking + uppercase preserved
BetaPillWrapper link supplies keyboard"chip text, body text"Wrapper linkChevron translate → opacityChip bg → Highlight; chevron → ButtonText
MonoChipNon-interactiveVerbatimN/AStaticTone bg → Highlight; fg → HighlightText
StatusPillNon-interactiveAvatar decorative; text plain; wrap in output/aria-live for live updatesN/AShimmer → static fillShimmer → system foreground
VerbPillNon-interactiveVerb verbatimN/AStaticBackground → Highlight; foreground → HighlightText

The cross-cutting rules

Every component above respects the same six global rules:

  1. Keyboard reachable. Tab order matches visual order. Enter / Space activate. Escape closes. Arrow keys navigate where applicable.
  2. Screen-reader correct. Semantic HTML first; ARIA only where semantics don't suffice. Live regions for status updates.
  3. Focus visible. :focus-visible (not :focus) ring on every interactive element. Canonical peach ring via --focus-ring.
  4. AA contrast. Every foreground-on-background pair meets WCAG AA (4.5:1 normal, 3:1 large). Verify with the color foundation contrast samples.
  5. Reduced motion. @media (prefers-reduced-motion: reduce) collapses animation to opacity transitions. Never disables interaction.
  6. Forced colors. Every component remains identifiable when the system overrides colors. Shape, spacing, semantic affordance survive.

Automated gates

GateWhereRuns
Token usage (no raw hex in MDX)check-design-drift.ts invariant 4CI on every PR
Component coverage (every export has MDX)check-design-drift.ts invariants 1 + 2CI on every PR
Token / TS↔CSS paritypackages/brand/test/tokens-sync.test.tsCI on every PR
Per-component a11y prose presencecheck-design-drift.ts invariant 7CI on every PR
Axe-core sweep on every rendered pageaxe-sweep.tsRun manually via bun run --filter design a11y
Per-viewport visual regressionChromatic (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 a11y

The 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

On this page