Accessibility (WCAG 2.2 AA)
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/expert-react-frontend-engineer/a11y.md |
| Description | Not 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
| Principle | What it means in this system |
|---|---|
| Perceivable | Color is never the only signal for state; minimum contrast ratios are met; media content has alternatives |
| Operable | All interactive components are keyboard-navigable; focus rings are always visible; no pointer-only interactions |
| Understandable | Labels, descriptions, and error messages are clear and attached to their controls with aria-labelledby or aria-describedby |
| Robust | Semantic HTML first; ARIA only where needed; screen reader testing is part of the review checklist |
Keyboard Contracts by Component Type
| Component | Expected keyboard behavior |
|---|---|
| Button | Space and Enter activate |
| Toggle / Switch | Space toggles |
| Radio group | Arrow keys move between options; Tab moves to/from the group |
| Checkbox | Space checks/unchecks |
| Select / Listbox | Arrow keys navigate; Enter or Space selects; Escape closes |
| Dialog / Modal | Tab cycles focusable elements; Escape closes; focus returns to trigger |
| Tooltip | Appears on hover and keyboard focus; Escape dismisses |
| Accordion | Enter or Space expands/collapses; Arrow keys navigate headers |
| Date picker | Full keyboard navigation within calendar; Escape closes |
| Combobox | Arrow keys navigate suggestions; Enter selects; Escape closes |
| CommandPalette | Global shortcut opens; Arrow keys move commands; Enter activates; Escape closes |
| DataGrid | Tab reaches toolbar/filters/controls; sortable headers expose sort state |
| SlideOutPanel / SlideOutWizard | Focus trapped within topmost panel; Tab/Shift+Tab wraps; Escape closes; focus returns to trigger |
| FileDropzone | Drop target and browse button keyboard-reachable; Enter/Space opens file picker; removal actions reachable |
| Dropdown wrappers | Trigger 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-hiddencontainers.
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-labeloraria-labelledbyonly when a visible label is not possible. - Use
aria-liveregions for toast notifications and status updates so screen readers announce without requiring focus. - Avoid
aria-hiddenon interactive content. - Test with VoiceOver (macOS/iOS) or NVDA (Windows) for dialog, dropdown, and form families.
Screen reader review checklist (before marking stable)
| Family | Components | Required checks |
|---|---|---|
| Dialog and slide-out flows | Dialog, SlideOutPanel, SlideOutWizard | Title and description announce correctly; focus lands inside; Escape closes; focus returns to trigger |
| Dropdown and selection | Select, SearchableSelect, DropdownMenu | Trigger announces expanded state; active option and selection state read correctly; Escape dismisses; focus returns |
| Form validation | TextField, TextArea, Checkbox, DatePicker | Labels announced; aria-invalid voiced when present; inline error messages associated; submission errors reviewable |
| Long-list and table | DataGrid, Table, LogViewer | Caption/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: reduceby compressing or eliminating timing. - Use the
useReducedMotionhook 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-motionbehavior usingcreateComponentDocs.
A11y Testing Pipeline (ADR-005)
| Gate | Tool | How |
|---|---|---|
| Automated axe checks | @storybook/addon-a11y | Run during Storybook review; CI runs pnpm test for critical stories |
| Keyboard testing | Manual | Tab through every interactive element; verify all keyboard contracts above |
| Screen reader testing | VoiceOver / NVDA | Required for dialog, dropdown, form families before stable promotion |
| Contrast | Design token validation | Semantic tones pre-validated; custom colors must meet 4.5:1 / 3:1 |
| Visual regression | Lost Pixel | pnpm 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-busyand 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-liveregions announced for dynamic content (toasts, status) - Dialog/drawer/panel traps focus and returns focus on close
-
prefers-reduced-motionrespected — no forced animation -
axeclean in Storybook a11y addon panel - Semantic HTML used throughout — no div soup with ARIA roles where native elements exist