Provides WCAG 2.1 AA compliance patterns for building inclusive frontend interfaces with proper semantic markup, keyboard navigation, focus management, and screen reader support.
Start with the correct HTML element. `` for actions, `
` for navigation, `` for navigation regions, `` for primary content. Native semantics are free, reliable, and require zero ARIA. Only reach for ARIA when HTML alone cannot convey the meaning.
Every interaction available to a mouse user must be available to a keyboard user. Tab, Escape, Enter, Space, Arrow keys — these are the vocabulary of keyboard interaction. Missing keyboard support is not a minor issue — it is a blocker for many users.
Color must never be the sole means of conveying information. Error states need icons + text, not just red borders. Status indicators need labels, not just colored dots. Always pair visual indicators with non-visual alternatives.
Accessibility Implementation Process
Use the checklist below and track your progress:
Progress:
- [ ] Step 1: Choose semantic elements
- [ ] Step 2: Implement keyboard navigation
- [ ] Step 3: Add ARIA where HTML falls short
- [ ] Step 4: Verify color and contrast
- [ ] Step 5: Test with assistive technology
Step 1: Choose semantic elements
Select the correct HTML element for each piece of UI:
- Interactive elements:
<button> — actions (submit, toggle, open menu)
<a href> — navigation to another page or location
<input>, <select>, <textarea> — form data entry
- Landmarks:
<header> — page or section header
<nav> — navigation region
<main> — primary content (one per page)
<aside> — tangentially related content
<footer> — page or section footer
- Structure:
<article> — self-contained composition
<section> — thematic grouping (must have a heading)
<details> / <summary> — native expand/collapse
- Heading hierarchy:
- One
<h1> per page
- Logical nesting:
h2 → h3 → h4
- Never skip levels (e.g.,
h2 → h4)
- Lists:
<ul> / <ol> for collections of items
<dl> for key-value pairs (definition lists)
If the component library wraps these elements, verify the rendered HTML output matches expectations using browser devtools.
Step 2: Implement keyboard navigation
All interactive elements must be focusable. Native interactive elements (<button>, <a>, <input>) are focusable by default. Custom interactive elements need tabindex="0".
-
Tab order must follow visual and logical reading order. Avoid tabindex values greater than 0 — they create unpredictable focus sequences.
-
Custom interactive components need explicit keyboard handlers:
| Component | Keyboard behavior |
|---|
| Button | Enter + Space to activate |
| Menu | Arrow keys to navigate items, Escape to close, Enter to select |
| Tabs | Left/Right arrows to switch tabs, Tab to leave the tab group |
| Dialog / Modal | Tab trapped inside, Escape to close |
| Accordion | Enter/Space to expand/collapse, Arrow keys between headers |
| Combobox | Arrow keys to navigate options, Enter to select, Escape to close |
-
Focus visibility — never remove the focus outline (outline: none) without providing a visible replacement. Custom focus styles must have at least 3:1 contrast against adjacent colors.
-
Programmatic focus management — move focus when context changes:
- Modal opens → focus the first focusable element inside
- Modal closes → return focus to the element that triggered it
- Route change → focus the new page heading or main content
- Dynamic content added → focus the new content or announce it via
aria-live
Step 3: Add ARIA where HTML falls short
Rule: prefer native HTML semantics. Only add ARIA when HTML cannot express the pattern.
Common patterns requiring ARIA:
- Icon-only buttons: Add
aria-label="Close" (or the appropriate action description).
- Expandable sections:
aria-expanded="true" or aria-expanded="false" on the trigger element.
- Live updates:
aria-live="polite" for non-urgent updates (data refreshed, filter applied). aria-live="assertive" for urgent updates (session expiring, critical error).
- Dialogs:
aria-modal="true", aria-labelledby pointing to the dialog title, aria-describedby pointing to the dialog description.
- Form errors:
aria-invalid="true" on the invalid field
aria-describedby pointing to the error message element
- Error container with
role="alert" or aria-live="assertive" for immediate announcement
- Loading states: Container with
role="status" for the loading message (e.g., "Loading results..."). Note: role="status" implicitly sets aria-live="polite" — no need to add both.
- Current page in navigation:
aria-current="page" on the active nav link.
- Progress indicators:
role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax.
Never do:
role="button" on a <div> — use <button> instead
- ARIA that duplicates native semantics (e.g.,
role="heading" on an <h2>)
aria-label on non-interactive, non-landmark elements (screen readers may ignore it)
Step 4: Verify color and contrast