Skip to content

Accessibility (WCAG 2.2 AA)

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/expert-react-frontend-engineer/a11y.md
DescriptionNot specified

Source Content

Accessibility (WCAG 2.2 AA)

The design system targets WCAG 2.2 Level AA for all stable components. Accessibility is a design constraint, not a checklist item bolted on at the end.

The Four Principles

PrincipleWhat it means in this system
PerceivableColor is never the only signal for state; minimum contrast ratios are met; media content has alternatives
OperableAll interactive components are keyboard-navigable; focus rings are always visible; no pointer-only interactions
UnderstandableLabels, descriptions, and error messages are clear and attached to their controls with aria-labelledby or aria-describedby
RobustSemantic HTML first; ARIA only where needed; screen reader testing is part of the review checklist

Keyboard Contracts by Component Type

ComponentExpected keyboard behavior
ButtonSpace and Enter activate
Toggle / SwitchSpace toggles
Radio groupArrow keys move between options; Tab moves to/from the group
CheckboxSpace checks/unchecks
Select / ListboxArrow keys navigate; Enter or Space selects; Escape closes
Dialog / ModalTab cycles focusable elements; Escape closes; focus returns to trigger
TooltipAppears on hover and keyboard focus; Escape dismisses
AccordionEnter or Space expands/collapses; Arrow keys navigate headers
Date pickerFull keyboard navigation within calendar; Escape closes
ComboboxArrow keys navigate suggestions; Enter selects; Escape closes
CommandPaletteGlobal shortcut opens; Arrow keys move commands; Enter activates; Escape closes
DataGridTab reaches toolbar/filters/controls; sortable headers expose sort state
SlideOutPanel / SlideOutWizardFocus trapped within topmost panel; Tab/Shift+Tab wraps; Escape closes; focus returns to trigger
FileDropzoneDrop target and browse button keyboard-reachable; Enter/Space opens file picker; removal actions reachable
Dropdown wrappersTrigger keyboard-reachable; Arrow moves options; Enter/Space commits; Escape closes

Focus Management Rules

  • Every interactive element must have a visible focus ring in both light and dark mode (focus-visible:ring-2 focus-visible:ring-ring).
  • Dialogs, drawers, and slide-out panels must trap focus within the overlay.
  • When an overlay closes, focus returns to the trigger element.
  • Portaled overlays must not be clipped by overflow-hidden containers.

Color and Contrast

  • Normal text: 4.5:1 minimum contrast ratio.
  • Large text and UI components (borders, icons conveying state): 3:1 minimum.
  • Color is never the only indicator of state. Always pair with an icon, text, or pattern.
  • Use semantic tones (info, success, warning, danger) — these are pre-validated for contrast across light and dark themes.

Semantic HTML First

Use semantic elements before reaching for ARIA:

// GOOD
<button type="button" onClick={handleDelete}>Delete</button>
<nav aria-label="Main navigation">...</nav>
<main>...</main>
<article aria-label={`Case file ${caseId}`}>...</article>
// BAD — div soup with role
<div role="button" onClick={handleDelete}>Delete</div>

ARIA fills semantic gaps — it doesn’t replace HTML. Common correct ARIA usage:

// Live regions for dynamic updates (toast notifications, status)
<div aria-live="polite" aria-atomic="true">{statusMessage}</div>
// Connecting error messages to form inputs
<TextField
id="email"
aria-describedby="email-error"
aria-invalid={!!errors.email}
/>
<p id="email-error" role="alert">{errors.email?.message}</p>
// Describing loading state
<button aria-busy={isPending} disabled={isPending}>
{isPending ? 'Saving…' : 'Save'}
</button>
// Groups of related controls
<div role="group" aria-labelledby="actions-label">
<span id="actions-label" className="sr-only">Case actions</span>
<button>Edit</button>
<button>Archive</button>
</div>

Screen Reader Guidance

  • Provide visible labels for all form controls; use aria-label or aria-labelledby only when a visible label is not possible.
  • Use aria-live regions for toast notifications and status updates so screen readers announce without requiring focus.
  • Avoid aria-hidden on interactive content.
  • Test with VoiceOver (macOS/iOS) or NVDA (Windows) for dialog, dropdown, and form families.

Screen reader review checklist (before marking stable)

FamilyComponentsRequired checks
Dialog and slide-out flowsDialog, SlideOutPanel, SlideOutWizardTitle and description announce correctly; focus lands inside; Escape closes; focus returns to trigger
Dropdown and selectionSelect, SearchableSelect, DropdownMenuTrigger announces expanded state; active option and selection state read correctly; Escape dismisses; focus returns
Form validationTextField, TextArea, Checkbox, DatePickerLabels announced; aria-invalid voiced when present; inline error messages associated; submission errors reviewable
Long-list and tableDataGrid, Table, LogViewerCaption/table name announced; sortable headers expose state changes; pagination understandable in browse and focus modes

Document: screen reader, OS, browser, and date of review in the PR when promoting a component family to stable.

Reduced Motion

  • All animated components respect prefers-reduced-motion: reduce by compressing or eliminating timing.
  • Use the useReducedMotion hook from the package root to conditionally disable animation in custom components.
import { useReducedMotion } from '@dmwd-io/design-system'
function AnimatedPanel({ isOpen }: { isOpen: boolean }) {
const prefersReduced = useReducedMotion()
return (
<div
style={{
transition: prefersReduced ? 'none' : 'transform 200ms ease-out',
transform: isOpen ? 'translateX(0)' : 'translateX(100%)',
}}
>
...
</div>
)
}
  • Motion docs in each component’s Storybook page must describe both full and reduced-motion behavior using createComponentDocs.

A11y Testing Pipeline (ADR-005)

GateToolHow
Automated axe checks@storybook/addon-a11yRun during Storybook review; CI runs pnpm test for critical stories
Keyboard testingManualTab through every interactive element; verify all keyboard contracts above
Screen reader testingVoiceOver / NVDARequired for dialog, dropdown, form families before stable promotion
ContrastDesign token validationSemantic tones pre-validated; custom colors must meet 4.5:1 / 3:1
Visual regressionLost Pixelpnpm test:visual — catches focus ring regressions across modes

Running axe in tests

import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no axe violations', async () => {
const { container } = render(<MyComponent {...mockProps} />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})

A11y Review Checklist (pre-ship)

Before marking any UI change ready:

  • All interactive elements reachable and operable via keyboard
  • Visible focus ring present in light and dark mode
  • All form controls have associated visible labels
  • Error messages associated with their inputs via aria-describedby
  • Loading/pending states use aria-busy and disable the control
  • Color is not the only indicator of any state
  • Contrast ratios meet 4.5:1 (text) and 3:1 (UI components)
  • aria-live regions announced for dynamic content (toasts, status)
  • Dialog/drawer/panel traps focus and returns focus on close
  • prefers-reduced-motion respected — no forced animation
  • axe clean in Storybook a11y addon panel
  • Semantic HTML used throughout — no div soup with ARIA roles where native elements exist