Accessibility
Decision Tree
Building UI → What needs a11y attention?
├─ Interactive element (button, link, form) → Keyboard + screen reader + focus
├─ Image or icon → Alt text or aria-hidden
├─ Custom component (dropdown, modal, tabs) → ARIA roles + keyboard patterns
├─ Color or styling → Contrast + not color-only indicators
├─ Dynamic content (toast, live region) → aria-live announcements
└─ Full page/app audit → Run checklist below
WCAG Quick Reference (Level AA)
| Principle |
Requirement |
Check |
| Perceivable |
Text alternatives for images |
All <img> have meaningful alt (or alt="" if decorative) |
|
Color contrast |
4.5:1 for normal text, 3:1 for large text |
|
Don't rely on color alone |
Use icons, patterns, or text alongside color |
|
Captions for video |
Provide captions and transcripts |
| Operable |
Keyboard accessible |
Every interactive element reachable and operable via keyboard |
|
Visible focus indicator |
:focus-visible styles on all interactive elements |
|
No keyboard traps |
User can always Tab out of any component |
|
Skip navigation |
"Skip to main content" link as first focusable element |
| Understandable |
Page language set |
<html lang="en"> |
|
Form labels |
Every input has a visible <label> with for attribute |
|
Error identification |
Errors described in text, not just red border |
| Robust |
Valid HTML |
Proper heading hierarchy (h1→h2→h3, no skipping) |
|
ARIA used correctly |
Only when native HTML can't do it |
Keyboard Patterns
| Component |
Expected Keyboard Behavior |
| Button |
Enter or Space activates |
| Link |
Enter navigates |
| Modal/Dialog |
Escape closes, focus trapped inside, returns focus on close |
| Dropdown/Menu |
Arrow keys navigate, Enter selects, Escape closes |
| Tabs |
Arrow keys switch tabs, Tab moves to panel content |
| Checkbox |
Space toggles |
| Form |
Tab moves between fields, Enter submits |
ARIA — Only When Needed
Rule #1: Don't use ARIA if native HTML works.
<button> not <div role="button">
<nav> not <div role="navigation">
<input type="checkbox"> not <div role="checkbox">
When ARIA IS needed
| Pattern |
ARIA |
| Custom dropdown |
role="listbox", role="option", aria-expanded, aria-activedescendant |
| Modal dialog |
role="dialog", aria-modal="true", aria-labelledby |
| Tabs |
role="tablist", role="tab", role="tabpanel", aria-selected |
| Toast/notification |
role="alert" or aria-live="polite" |
| Loading state |
aria-busy="true", aria-live="polite" with status text |
| Icon button |
aria-label="Close" (no visible text) |
| Progress |
role="progressbar", aria-valuenow, aria-valuemin, aria-valuemax |
Form Accessibility
<!-- Always: visible label linked to input -->
<label for="email">Email address</label>
<input id="email" type="email" required aria-describedby="email-error">
<span id="email-error" role="alert">Please enter a valid email</span>
<!-- Never: placeholder as the only label -->
<input placeholder="Email"> <!-- Screen reader can't reliably read this -->
Checklist:
Color Contrast
| Element |
Minimum Ratio (AA) |
Tools |
| Normal text (<18px) |
4.5:1 |
WebAIM Contrast Checker |
| Large text (≥18px bold or ≥24px) |
3:1 |
Chrome DevTools |
| UI components (borders, icons) |
3:1 |
Figma a11y plugins |
Don't rely on color alone:
BAD: Red text for errors (colorblind users can't see it)
GOOD: Red text + error icon + descriptive message
Testing
# Automated (catches ~30% of issues)
npx axe-core-cli http://localhost:3000 # axe accessibility scanner
npx pa11y http://localhost:3000 # WCAG compliance checker
# Chrome DevTools
# Lighthouse → Accessibility audit
# Elements → Accessibility pane (inspect ARIA tree)
Manual testing (catches the rest):
- Tab through the entire page — can you reach and operate everything?
- Turn off CSS — does the content still make sense?
- Use a screen reader (VoiceOver on Mac, NVDA on Windows)
- Zoom to 200% — does the layout break?
Anti-Patterns
| Anti-Pattern |
Fix |
<div onclick> instead of <button> |
Use semantic HTML elements |
| Missing alt text on images |
Add alt="description" or alt="" if decorative |
Removing focus outlines (:focus { outline: none }) |
Style :focus-visible instead of removing |
tabindex="5" (positive tabindex) |
Use tabindex="0" or -1 only |
aria-label on elements that have visible text |
Remove — screen readers read visible text |
| Color-only error indication |
Add text + icon alongside color |
| Auto-playing media |
Never autoplay, or provide immediate stop control |
| Missing skip navigation |
Add "Skip to main content" as first link |
| Custom components without keyboard support |
Implement full keyboard pattern |
role="button" without Enter/Space handling |
Use native <button> instead |
1---2name: accessibility3description: Web accessibility checklist covering WCAG guidelines, ARIA patterns, keyboard navigation, color contrast, screen reader support, and form accessibility. Use when building UI components, reviewing frontend code, or when accessibility is mentioned.4---56# Accessibility78## Decision Tree910```11Building UI → What needs a11y attention?12 ├─ Interactive element (button, link, form) → Keyboard + screen reader + focus13 ├─ Image or icon → Alt text or aria-hidden14 ├─ Custom component (dropdown, modal, tabs) → ARIA roles + keyboard patterns15 ├─ Color or styling → Contrast + not color-only indicators16 ├─ Dynamic content (toast, live region) → aria-live announcements17 └─ Full page/app audit → Run checklist below18```1920## WCAG Quick Reference (Level AA)2122| Principle | Requirement | Check |23|-----------|------------|-------|24| **Perceivable** | Text alternatives for images | All `<img>` have meaningful `alt` (or `alt=""` if decorative) |25| | Color contrast | 4.5:1 for normal text, 3:1 for large text |26| | Don't rely on color alone | Use icons, patterns, or text alongside color |27| | Captions for video | Provide captions and transcripts |28| **Operable** | Keyboard accessible | Every interactive element reachable and operable via keyboard |29| | Visible focus indicator | `:focus-visible` styles on all interactive elements |30| | No keyboard traps | User can always Tab out of any component |31| | Skip navigation | "Skip to main content" link as first focusable element |32| **Understandable** | Page language set | `<html lang="en">` |33| | Form labels | Every input has a visible `<label>` with `for` attribute |34| | Error identification | Errors described in text, not just red border |35| **Robust** | Valid HTML | Proper heading hierarchy (h1→h2→h3, no skipping) |36| | ARIA used correctly | Only when native HTML can't do it |3738## Keyboard Patterns3940| Component | Expected Keyboard Behavior |41|-----------|--------------------------|42| Button | `Enter` or `Space` activates |43| Link | `Enter` navigates |44| Modal/Dialog | `Escape` closes, focus trapped inside, returns focus on close |45| Dropdown/Menu | `Arrow keys` navigate, `Enter` selects, `Escape` closes |46| Tabs | `Arrow keys` switch tabs, `Tab` moves to panel content |47| Checkbox | `Space` toggles |48| Form | `Tab` moves between fields, `Enter` submits |4950## ARIA — Only When Needed5152```53Rule #1: Don't use ARIA if native HTML works.54 <button> not <div role="button">55 <nav> not <div role="navigation">56 <input type="checkbox"> not <div role="checkbox">57```5859### When ARIA IS needed6061| Pattern | ARIA |62|---------|------|63| Custom dropdown | `role="listbox"`, `role="option"`, `aria-expanded`, `aria-activedescendant` |64| Modal dialog | `role="dialog"`, `aria-modal="true"`, `aria-labelledby` |65| Tabs | `role="tablist"`, `role="tab"`, `role="tabpanel"`, `aria-selected` |66| Toast/notification | `role="alert"` or `aria-live="polite"` |67| Loading state | `aria-busy="true"`, `aria-live="polite"` with status text |68| Icon button | `aria-label="Close"` (no visible text) |69| Progress | `role="progressbar"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` |7071## Form Accessibility7273```html74<!-- Always: visible label linked to input -->75<label for="email">Email address</label>76<input id="email" type="email" required aria-describedby="email-error">77<span id="email-error" role="alert">Please enter a valid email</span>7879<!-- Never: placeholder as the only label -->80<input placeholder="Email"> <!-- Screen reader can't reliably read this -->81```8283**Checklist:**84- [ ] Every input has a `<label>` with `for` attribute85- [ ] Required fields indicated (not just by color)86- [ ] Error messages linked via `aria-describedby`87- [ ] Error messages use `role="alert"` for screen reader announcement88- [ ] Submit button has clear text (not just an icon)89- [ ] Autocomplete attributes set (`autocomplete="email"`, etc.)9091## Color Contrast9293| Element | Minimum Ratio (AA) | Tools |94|---------|-------------------|-------|95| Normal text (<18px) | 4.5:1 | WebAIM Contrast Checker |96| Large text (≥18px bold or ≥24px) | 3:1 | Chrome DevTools |97| UI components (borders, icons) | 3:1 | Figma a11y plugins |9899**Don't rely on color alone:**100```101BAD: Red text for errors (colorblind users can't see it)102GOOD: Red text + error icon + descriptive message103```104105## Testing106107```bash108# Automated (catches ~30% of issues)109npx axe-core-cli http://localhost:3000 # axe accessibility scanner110npx pa11y http://localhost:3000 # WCAG compliance checker111112# Chrome DevTools113# Lighthouse → Accessibility audit114# Elements → Accessibility pane (inspect ARIA tree)115```116117**Manual testing (catches the rest):**118- Tab through the entire page — can you reach and operate everything?119- Turn off CSS — does the content still make sense?120- Use a screen reader (VoiceOver on Mac, NVDA on Windows)121- Zoom to 200% — does the layout break?122123## Anti-Patterns124125| Anti-Pattern | Fix |126|-------------|-----|127| `<div onclick>` instead of `<button>` | Use semantic HTML elements |128| Missing alt text on images | Add `alt="description"` or `alt=""` if decorative |129| Removing focus outlines (`:focus { outline: none }`) | Style `:focus-visible` instead of removing |130| `tabindex="5"` (positive tabindex) | Use `tabindex="0"` or `-1` only |131| `aria-label` on elements that have visible text | Remove — screen readers read visible text |132| Color-only error indication | Add text + icon alongside color |133| Auto-playing media | Never autoplay, or provide immediate stop control |134| Missing skip navigation | Add "Skip to main content" as first link |135| Custom components without keyboard support | Implement full keyboard pattern |136| `role="button"` without Enter/Space handling | Use native `<button>` instead |