Accessibility Patterns
WCAG 2.2 POUR Principles
Perceivable
- All non-text content (images, icons, SVGs) has text alternatives. Decorative images use
alt="" or aria-hidden="true"
- Color is never the sole means of conveying information (error states, status indicators)
- Text contrast meets 4.5:1 for normal text, 3:1 for large text (WCAG 1.4.3)
- Non-text contrast meets 3:1 for UI components and graphical objects (WCAG 1.4.11)
- Content reflows without horizontal scrolling at 320px width / 400% zoom (WCAG 1.4.10)
- Text spacing can be overridden without loss of content (WCAG 1.4.12)
- Content remains visible and functional in forced colors mode (
forced-colors: active) and high contrast mode (prefers-contrast: more)
Operable
- All interactive elements reachable and operable via keyboard alone
- No keyboard traps -- users can always Tab away from any element
- Focus order follows logical reading sequence
- Focus indicator is visible on every interactive element (WCAG 2.4.7, enhanced 2.4.13)
- Skip navigation link present and functional for bypassing repeated content (WCAG 2.4.1)
- Target size minimum 24x24 CSS pixels for pointer inputs (WCAG 2.5.8)
- Motion-triggered actions have alternatives and respect
prefers-reduced-motion (WCAG 2.3.3)
Understandable
- Page language declared via
lang attribute on <html>
- Form inputs have visible labels (not just placeholder text)
- Error messages identify the field and describe how to fix the error
- Consistent navigation and identification across pages
Robust
- HTML nesting and unique IDs correct for accessibility tree integrity (duplicate IDs break ARIA references, improper nesting confuses assistive technologies)
- Custom components use correct ARIA roles, states, and properties
- Status messages use
aria-live regions (not focus changes) to announce updates
- Content works across browsers and assistive technologies
Semantic HTML Before ARIA
Prefer native HTML elements over ARIA roles. ARIA should supplement, not replace, semantic HTML.
| Instead of |
Use |
<div role="button"> |
<button> |
<span role="link"> |
<a href> |
<div role="heading" aria-level="2"> |
<h2> |
<div role="navigation"> |
<nav> |
<div role="main"> |
<main> |
<div role="checkbox"> |
<input type="checkbox"> |
ARIA Anti-Patterns (CRITICAL)
aria-label on non-interactive, non-landmark elements (screen readers may ignore it)
aria-hidden="true" on focusable elements (creates ghost focus)
- Redundant roles on semantic HTML (
<nav role="navigation">, <button role="button">)
role="presentation" or role="none" on elements with focusable descendants
- Using
aria-expanded without a corresponding controlled region (aria-controls)
aria-live="assertive" for non-urgent updates (use polite for most cases)
Interactive Component Patterns
Modals/Dialogs
- Focus moves into modal on open
- Focus is trapped inside modal (Tab cycles within)
- Escape closes the modal
- Focus returns to trigger element on close
- Background content has
aria-hidden="true" or inert
Tabs
- Tab key moves into/out of tablist; Arrow keys switch tabs
aria-selected on active tab; aria-controls links tab to panel
- Home/End move to first/last tab
Menus
- Arrow keys navigate menu items
- Enter/Space activates menu item
- Escape closes menu and returns focus to trigger
role="menu" on container; role="menuitem" on items
Carousels/Sliders
- Arrow keys move between slides
- Pause/stop control available and keyboard accessible
- Current position announced (e.g., "Slide 2 of 5")
- Auto-advancing respects
prefers-reduced-motion
Data Tables
- Headers associated with cells via
scope or headers attributes
- Caption or
aria-label describes table purpose
- Sortable columns operable via keyboard with sort direction announced
Forms
- Every input has a programmatically associated
<label> (via for/id or wrapping)
- Required fields use
aria-required="true" or required attribute
- Error messages associated via
aria-describedby
- Form validation errors announced to screen readers (via
aria-live or focus management)
Custom Widgets
- Follow WAI-ARIA Authoring Practices for keyboard interaction patterns
- Manage focus explicitly for composite widgets (menus, listboxes, trees)
- Announce state changes (expanded/collapsed, selected/deselected, checked/unchecked)
Dynamic Content
- Route changes in SPAs announce the new page title to screen readers
- Loading states communicated via
aria-busy or live region announcements
- Toast/notification content uses
role="status" or aria-live="polite"
- Content added/removed dynamically updates the accessibility tree
Common Failure Patterns
- Empty buttons or links (no text content, no
aria-label)
- Form inputs with placeholder as the only label (placeholder disappears on input)
- Auto-playing media without pause controls
- Infinite scroll without keyboard-accessible "load more" alternative
- Drag-and-drop without keyboard alternative
- Custom select/dropdown that doesn't support arrow key navigation
- Focus lost after dynamic content updates (modal close, item deletion, SPA navigation)
tabindex values > 0 creating unpredictable tab order
- Click handlers on non-interactive elements (
<div onClick>) without role and keyboard support
Framework-Specific Pitfalls
- React: Portals can break focus order;
useEffect cleanup must restore focus; key changes can reset focus
- Vue: Transition groups may skip screen reader announcements;
v-if removal loses focus
- SPA Routers: Route changes do not announce page title by default -- must implement manually
Automated vs Manual Testing
Automated tools (axe-core, Lighthouse) catch ~30% of accessibility issues. They detect missing alt text, low contrast, missing labels. They CANNOT detect: logical reading order, keyboard interaction quality, focus management correctness, ARIA misuse in context, cognitive clarity of content. Both are required for genuine compliance.
1---2name: accessibility-patterns-23description: Accessibility patterns including WCAG 2.2 compliance, ARIA correctness, keyboard navigation, and screen reader compatibility. Use when writing or reviewing UI code with interactive elements, forms, or dynamic content.4---5
6# Accessibility Patterns
7
8## WCAG 2.2 POUR Principles
9
10### Perceivable
11- All non-text content (images, icons, SVGs) has text alternatives. Decorative images use `alt=""` or `aria-hidden="true"`
12- Color is never the sole means of conveying information (error states, status indicators)
13- Text contrast meets 4.5:1 for normal text, 3:1 for large text (WCAG 1.4.3)
14- Non-text contrast meets 3:1 for UI components and graphical objects (WCAG 1.4.11)
15- Content reflows without horizontal scrolling at 320px width / 400% zoom (WCAG 1.4.10)
16- Text spacing can be overridden without loss of content (WCAG 1.4.12)
17- Content remains visible and functional in forced colors mode (`forced-colors: active`) and high contrast mode (`prefers-contrast: more`)
18
19### Operable
20- All interactive elements reachable and operable via keyboard alone
21- No keyboard traps -- users can always Tab away from any element
22- Focus order follows logical reading sequence
23- Focus indicator is visible on every interactive element (WCAG 2.4.7, enhanced 2.4.13)
24- Skip navigation link present and functional for bypassing repeated content (WCAG 2.4.1)
25- Target size minimum 24x24 CSS pixels for pointer inputs (WCAG 2.5.8)
26- Motion-triggered actions have alternatives and respect `prefers-reduced-motion` (WCAG 2.3.3)
27
28### Understandable
29- Page language declared via `lang` attribute on `<html>`
30- Form inputs have visible labels (not just placeholder text)
31- Error messages identify the field and describe how to fix the error
32- Consistent navigation and identification across pages
33
34### Robust
35- HTML nesting and unique IDs correct for accessibility tree integrity (duplicate IDs break ARIA references, improper nesting confuses assistive technologies)
36- Custom components use correct ARIA roles, states, and properties
37- Status messages use `aria-live` regions (not focus changes) to announce updates
38- Content works across browsers and assistive technologies
39
40## Semantic HTML Before ARIA
41
42Prefer native HTML elements over ARIA roles. ARIA should supplement, not replace, semantic HTML.
43
44| Instead of | Use |
45|------------|-----|
46| `<div role="button">` | `<button>` |
47| `<span role="link">` | `<a href>` |
48| `<div role="heading" aria-level="2">` | `<h2>` |
49| `<div role="navigation">` | `<nav>` |
50| `<div role="main">` | `<main>` |
51| `<div role="checkbox">` | `<input type="checkbox">` |
52
53## ARIA Anti-Patterns (CRITICAL)
54
55- `aria-label` on non-interactive, non-landmark elements (screen readers may ignore it)
56- `aria-hidden="true"` on focusable elements (creates ghost focus)
57- Redundant roles on semantic HTML (`<nav role="navigation">`, `<button role="button">`)
58- `role="presentation"` or `role="none"` on elements with focusable descendants
59- Using `aria-expanded` without a corresponding controlled region (`aria-controls`)
60- `aria-live="assertive"` for non-urgent updates (use `polite` for most cases)
61
62## Interactive Component Patterns
63
64### Modals/Dialogs
65- Focus moves into modal on open
66- Focus is trapped inside modal (Tab cycles within)
67- Escape closes the modal
68- Focus returns to trigger element on close
69- Background content has `aria-hidden="true"` or `inert`
70
71### Tabs
72- Tab key moves into/out of tablist; Arrow keys switch tabs
73- `aria-selected` on active tab; `aria-controls` links tab to panel
74- Home/End move to first/last tab
75
76### Menus
77- Arrow keys navigate menu items
78- Enter/Space activates menu item
79- Escape closes menu and returns focus to trigger
80- `role="menu"` on container; `role="menuitem"` on items
81
82### Carousels/Sliders
83- Arrow keys move between slides
84- Pause/stop control available and keyboard accessible
85- Current position announced (e.g., "Slide 2 of 5")
86- Auto-advancing respects `prefers-reduced-motion`
87
88### Data Tables
89- Headers associated with cells via `scope` or `headers` attributes
90- Caption or `aria-label` describes table purpose
91- Sortable columns operable via keyboard with sort direction announced
92
93### Forms
94- Every input has a programmatically associated `<label>` (via `for`/`id` or wrapping)
95- Required fields use `aria-required="true"` or `required` attribute
96- Error messages associated via `aria-describedby`
97- Form validation errors announced to screen readers (via `aria-live` or focus management)
98
99### Custom Widgets
100- Follow WAI-ARIA Authoring Practices for keyboard interaction patterns
101- Manage focus explicitly for composite widgets (menus, listboxes, trees)
102- Announce state changes (expanded/collapsed, selected/deselected, checked/unchecked)
103
104## Dynamic Content
105
106- Route changes in SPAs announce the new page title to screen readers
107- Loading states communicated via `aria-busy` or live region announcements
108- Toast/notification content uses `role="status"` or `aria-live="polite"`
109- Content added/removed dynamically updates the accessibility tree
110
111## Common Failure Patterns
112
113- Empty buttons or links (no text content, no `aria-label`)
114- Form inputs with placeholder as the only label (placeholder disappears on input)
115- Auto-playing media without pause controls
116- Infinite scroll without keyboard-accessible "load more" alternative
117- Drag-and-drop without keyboard alternative
118- Custom select/dropdown that doesn't support arrow key navigation
119- Focus lost after dynamic content updates (modal close, item deletion, SPA navigation)
120- `tabindex` values > 0 creating unpredictable tab order
121- Click handlers on non-interactive elements (`<div onClick>`) without role and keyboard support
122
123## Framework-Specific Pitfalls
124
125- **React**: Portals can break focus order; `useEffect` cleanup must restore focus; `key` changes can reset focus
126- **Vue**: Transition groups may skip screen reader announcements; `v-if` removal loses focus
127- **SPA Routers**: Route changes do not announce page title by default -- must implement manually
128
129## Automated vs Manual Testing
130
131Automated tools (axe-core, Lighthouse) catch ~30% of accessibility issues. They detect missing alt text, low contrast, missing labels. They CANNOT detect: logical reading order, keyboard interaction quality, focus management correctness, ARIA misuse in context, cognitive clarity of content. Both are required for genuine compliance.