| name | accessibility |
| description | WCAG 2.1 AA patterns for inclusive UI. Load when building components needing a11y, ARIA, or keyboard nav |
| user-invocable | false |
Web Accessibility (a11y)
Actionable WCAG 2.1 AA patterns for building inclusive UI — semantic HTML, keyboard navigation, ARIA, focus management, color contrast, and testing checklists.
When to Use This Skill
- Building or reviewing interactive UI components (forms, modals, menus, tabs)
- Auditing existing markup for accessibility compliance
- Implementing keyboard navigation or focus management
- Adding ARIA attributes to custom widgets
- Checking color contrast ratios
- Writing accessible error handling and live announcements
WCAG Principles: POUR
| Principle | Description |
|---|
| Perceivable | Content can be perceived through different senses |
| Operable | Interface can be operated by all users |
| Understandable | Content and interface are understandable |
| Robust | Content works with assistive technologies |
Methodology
Phase 1: Semantic Structure
Use native HTML elements before reaching for ARIA:
<div role="button" tabindex="0">Click me</div>
<button>Click me</button>
Landmark regions:
<header></header>
<nav aria-label="Main"></nav>
<main id="main-content"></main>
<aside></aside>
<footer></footer>
Skip link (first focusable element):
<a href="#main-content" class="skip-link">Skip to main content</a>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus { top: 0; }
Phase 2: Keyboard Navigation
All interactive elements must be keyboard-operable:
| Key | Action |
|---|
Tab | Move to next interactive element |
Shift+Tab | Move to previous interactive element |
Enter / Space | Activate focused control |
Arrow | Navigate within composite widgets (tabs, menus, grids) |
Escape | Close dialogs, menus, popovers |
Rules:
- Non-interactive elements must NOT have
tabindex (except tabindex="-1" for programmatic focus targets like headings)
- Hidden elements must not be focusable
- Focus must never get trapped without an escape mechanism
Focus visibility:
:focus-visible {
outline: 2px solid var(--color-focus, #005fcc);
outline-offset: 2px;
}
Roving tabindex for composite widgets (tabs, grids):
- Active child:
tabindex="0"
- All other children:
tabindex="-1"
- Arrow keys move focus and update tabindex values
Phase 3: ARIA & Labeling
Text alternatives:
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
<img src="border.png" alt="" role="presentation">
<button aria-label="Close dialog">
<svg aria-hidden="true"></svg>
</button>
ARIA states for interactive widgets:
<button aria-expanded="false" aria-controls="panel-1">Details</button>
<div id="panel-1" hidden>Content</div>
<input aria-invalid="true" aria-describedby="email-error">
<p id="email-error" role="alert">Please enter a valid email</p>
<button aria-busy="true" aria-live="polite" disabled>Loading...</button>
Custom tabs pattern:
<div role="tablist" aria-label="Product info">
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">Desc</button>
<button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Reviews</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</div>
Phase 4: Color & Contrast
| Element | AA Minimum Ratio |
|---|
| Normal text (< 18px / < 14px bold) | 4.5:1 |
| Large text (>= 18px / >= 14px bold) | 3:1 |
| UI components & graphics | 3:1 |
Never rely on color alone to convey meaning:
<input style="border-color: red">
<input aria-invalid="true" aria-describedby="err">
<span id="err">
<svg aria-hidden="true"></svg>
Please enter a valid email
</span>
Phase 5: Forms & Error Handling
<label for="email">Email address</label>
<input type="email" id="email" autocomplete="email" required
aria-describedby="email-hint">
<p id="email-hint">We'll never share your email</p>
On validation failure:
- Set
aria-invalid="true" on the field
- Associate error message via
aria-describedby
- Use
role="alert" or aria-live="assertive" on the error container
- Focus the first invalid field on form submit
Phase 6: Dynamic Content & Live Regions
<div aria-live="polite" aria-atomic="true">
3 items in your cart
</div>
<div role="alert" aria-live="assertive">
Connection lost. Reconnecting...
</div>
Phase 7: Motion & Timing
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
- Provide controls to pause/stop auto-updating content
- Allow users to extend time limits (session warnings)
Focus Management Patterns
Modal dialog focus trap:
- On open: save previously focused element, focus first focusable child
- Tab/Shift+Tab cycles within modal (trap)
- Escape closes the modal
- On close: restore focus to the saved element
Route transitions (SPA):
- After navigation, move focus to the new page's heading or main content
- Announce route changes via a live region
Audit Checklist
Critical (fix immediately)
Serious (fix before release)
Moderate (fix soon)
Anti-Patterns
- ❌ Div soup — Using
<div> or <span> for interactive elements instead of <button>, <a>, <input>
- ❌ Outline removal —
*:focus { outline: none } without replacement focus styles
- ❌ Placeholder-as-label — Using
placeholder as the only label (disappears on input, poor contrast)
- ❌ ARIA overuse — Adding redundant roles to semantic elements (
<nav role="navigation"> is unnecessary)
- ❌ tabindex > 0 — Positive tabindex values break natural tab order
- ✅ Progressive enhancement — Semantic HTML first, ARIA only for custom widgets