WCAG 2.1 Level AA — satisfying US Federal Section 508 (Revised 2018) and State of Oregon digital accessibility requirements.
| Tool | Scope | How to Run |
|---|---|---|
eslint-plugin-jsx-a11y |
Static JSX analysis (missing labels, invalid ARIA, click-without-keyboard) | npx eslint . |
vitest-axe |
Runtime axe-core scans of rendered component trees | npx vitest run (a11y tests in src/test/a11y.test.jsx) |
Automated tools catch approximately 30-50% of WCAG issues. Manual testing is required for the remainder.
- Keyboard-only navigation: Tab through every view without a mouse. All interactive elements must be reachable and operable.
- Screen reader verification: Test with VoiceOver (macOS) or NVDA (Windows). Modals must announce title and description. Live regions must announce notices and search results.
- Zoom/reflow: Verify content at 200% text zoom and 320px viewport width.
- Color contrast: Verify all text/background combinations meet 4.5:1 (normal text) or 3:1 (large text, UI components).
| Key | Action |
|---|---|
Tab / Shift+Tab |
Move between interactive elements |
← / → |
Navigate between phase tabs |
Escape |
Close any open modal |
Ctrl/Cmd+Z |
Undo |
Ctrl/Cmd+Shift+Z |
Redo |
? |
Open keyboard shortcuts reference |
Enter |
Submit forms, activate buttons |
- Skip link: Hidden link appears on first Tab press — jumps to main content area.
- Focus trap: Modals trap focus within the dialog until closed. Focus returns to the trigger element on close.
- Focus visible: All interactive elements show a 2px solid outline on
:focus-visible.
- Dark theme: Primary text
#e8e5dcon background#0f0f0f— contrast ratio ~18:1 (AAA). - Disabled elements: Text
#b0b0b0on#1a1a1a— contrast ratio ~5.5:1 (AA). - Status indicators: All color-coded badges include text labels. Color is never the sole means of conveying information.
- Focus ring:
#e8e5dcoutline — visible against all background colors.
The application respects prefers-reduced-motion: reduce:
- All CSS animations are suppressed (duration set to 0.01ms).
- Framer Motion animations are disabled via
useReducedMotion(). - Hover transforms are removed.
- Landmarks:
<main>,<nav>(sidebar), skip link to main content. - Headings: Single
<h1>per view. Logical heading hierarchy within phase/artifact content. - Forms: Related controls wrapped in
<fieldset>with<legend>(visually hidden where appropriate). - Tables: All data tables include
<caption>(sr-only),<th scope="col">, and proper<thead>/<tbody>structure. - Modals:
role="dialog",aria-modal="true",aria-labelledby(title),aria-describedby(body). - Tabs: Phase navigation uses
role="tablist"→role="tab"witharia-selected,aria-controls, and arrow key navigation. - Live regions: Notices use
aria-live="assertive". Search results usearia-live="polite".
-
Dashboard widget drag-and-drop: Layout rearrangement via react-grid-layout is mouse-only. All widget content remains keyboard accessible. Widgets are marked as
role="region"witharia-labelfor screen reader orientation. -
Markdown editor: The toolbar buttons for bold/italic/link formatting are keyboard accessible, but the live preview does not announce changes to screen readers in real-time.
-
Color contrast in phase badges: Phase-specific background colors (defined per plan template) may not all meet 3:1 non-text contrast against adjacent surfaces. Badge text always meets 4.5:1.
| Category | Criteria Covered | Notes |
|---|---|---|
| Perceivable (1.x) | 1.1.1, 1.3.1, 1.3.2, 1.3.3, 1.3.4, 1.3.5, 1.4.1, 1.4.3, 1.4.4, 1.4.10, 1.4.11, 1.4.12, 1.4.13 | Text alternatives, structure, contrast, reflow |
| Operable (2.x) | 2.1.1, 2.1.2, 2.1.4, 2.4.1, 2.4.2, 2.4.3, 2.4.5, 2.4.6, 2.4.7 | Keyboard, skip link, focus, titles |
| Understandable (3.x) | 3.1.1, 3.2.1, 3.2.3, 3.2.4, 3.3.1, 3.3.2, 3.3.3, 3.3.4 | Language, consistency, errors |
| Robust (4.x) | 4.1.2, 4.1.3 | Name/role/value, status messages |
When adding new components:
- Use semantic HTML (
<button>,<a>,<input>,<table>) over<div>with click handlers. - All interactive elements must be keyboard accessible (Tab + Enter/Space).
- Icon-only buttons require
aria-label. - Form groups require
<fieldset className="a11y-group">with<legend>. - Data tables require
<caption className="sr-only">andscope="col"on headers. - Modals require
role="dialog",aria-modal="true",aria-labelledby,aria-describedby. - Dynamic content changes require
aria-liveregions. - Run
npx eslint .before committing — jsx-a11y rules will catch common issues. - Add axe scans for new major components in
src/test/a11y.test.jsx.