accessibility-implementation
WCAG 2.1/2.2 compliance, ARIA patterns, keyboard nav, focus management, a11y testing. Use when implementing accessible components or user mentions WCAG/ARIA/screen readers.
Works with
---
name: accessibility-implementation
description: WCAG 2.1/2.2 compliance, ARIA patterns, keyboard nav, focus management, a11y testing. Use when implementing accessible components or user mentions WCAG/ARIA/screen readers.
license: MIT
---
# Accessibility Implementation
Technical implementation of WCAG guidelines, ARIA patterns, and assistive technology support.
## When to Use This Skill
| Use this skill when... | Use design-tokens instead when... |
|---|---|
| Implementing WCAG 2.1/2.2 success criteria in code | Setting up CSS custom properties or theme systems |
| Adding ARIA roles, states, or live regions | Defining semantic colour tokens used by themes |
| Wiring keyboard navigation, focus traps, or skip links | Organizing primitive/semantic/component token tiers |
| Auditing components with axe-core, jest-axe, or Playwright | Implementing light/dark mode token overrides |
## Core Expertise
- **WCAG Compliance**: Implementing WCAG 2.1/2.2 success criteria in code
- **ARIA Patterns**: Correct usage of roles, states, and properties
- **Keyboard Navigation**: Focus management, key handlers, logical tab order
- **Screen Readers**: Content structure, announcements, live regions
- **Testing**: Automated and manual accessibility testing
## WCAG Quick Reference
### Level A (Must Have)
| Criterion | Implementation |
|-----------|----------------|
| 1.1.1 Non-text Content | `alt` for images, labels for inputs |
| 1.3.1 Info and Relationships | Semantic HTML, ARIA relationships |
| 2.1.1 Keyboard | All interactive elements keyboard accessible |
| 2.4.1 Bypass Blocks | Skip links, landmarks |
| 4.1.2 Name, Role, Value | ARIA labels, roles for custom widgets |
### Level AA (Should Have)
| Criterion | Implementation |
|-----------|----------------|
| 1.4.3 Contrast (Minimum) | 4.5:1 text, 3:1 large text |
| 1.4.11 Non-text Contrast | 3:1 for UI components |
| 2.4.6 Headings and Labels | Descriptive, hierarchical headings |
| 2.4.7 Focus Visible | Visible focus indicator (2px+ outline) |
## ARIA Patterns
### Buttons and Links
```html
<!-- Custom button -->
<div role="button" tabindex="0"
aria-pressed="false"
onkeydown="handleKeyDown(event)">
Toggle Feature
</div>
<!-- Icon button (needs accessible name) -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<!-- Link vs button -->
<!-- Use link for navigation, button for actions -->
<a href="/page">Go to page</a>
<button type="button">Submit form</button>
```
### Form Controls
```html
<!-- Input with label -->
<label for="email">Email address</label>
<input id="email" type="email"
aria-describedby="email-hint email-error"
aria-invalid="true"
required>
<div id="email-hint">We'll never share your email</div>
<div id="email-error" role="alert">Please enter a valid email</div>
<!-- Checkbox group -->
<fieldset>
<legend>Notification preferences</legend>
<label><input type="checkbox" name="notif" value="email"> Email</label>
<label><input type="checkbox" name="notif" value="sms"> SMS</label>
</fieldset>
<!-- Combobox (autocomplete) -->
<label for="country">Country</label>
<input id="country"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="country-listbox">
<ul id="country-listbox" role="listbox" hidden>
<li role="option" id="opt-us">United States</li>
<li role="option" id="opt-uk">United Kingdom</li>
</ul>
```
### Modal Dialog
```html
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc">
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-desc">Are you sure you want to proceed?</p>
<button>Cancel</button>
<button>Confirm</button>
</div>
```
```typescript
// Focus trap implementation
function trapFocus(dialog: HTMLElement) {
const focusable = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') {
closeDialog();
}
});
// Move focus to first element
first.focus();
}
```
### Tabs
```html
<div role="tablist" aria-label="Settings tabs">
<button role="tab"
id="tab-1"
aria-selected="true"
aria-controls="panel-1">
General
</button>
<button role="tab"
id="tab-2"
aria-selected="false"
aria-controls="panel-2"
tabindex="-1">
Privacy
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
General settings content
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
Privacy settings content
</div>
```
```typescript
// Tab keyboard navigation
tablist.addEventListener('keydown', (e) => {
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const current = tabs.indexOf(document.activeElement as Element);
let next: number;
switch (e.key) {
case 'ArrowRight':
next = (current + 1) % tabs.length;
break;
case 'ArrowLeft':
next = (current - 1 + tabs.length) % tabs.length;
break;
case 'Home':
next = 0;
break;
case 'End':
next = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
(tabs[next] as HTMLElement).focus();
activateTab(tabs[next]);
});
```
### Live Regions
```html
<!-- Status messages -->
<div role="status" aria-live="polite">
Form saved successfully
</div>
<!-- Alerts (interrupts) -->
<div role="alert" aria-live="assertive">
Error: Connection lost
</div>
<!-- Progress updates -->
<div aria-live="polite" aria-atomic="true">
Loading: 45% complete
</div>
```
## Keyboard Navigation
### Standard Key Bindings
| Key | Behavior |
|-----|----------|
| Tab | Move to next focusable element |
| Shift+Tab | Move to previous focusable element |
| Enter/Space | Activate button, select option |
| Escape | Close modal, cancel operation |
| Arrow keys | Navigate within component (tabs, menu, listbox) |
| Home/End | Go to first/last item in list |
### Focus Management
```typescript
// Return focus after modal close
const triggerElement = document.activeElement;
openModal();
// On close:
closeModal();
triggerElement?.focus();
// Move focus to error
function showValidationErrors() {
const firstError = document.querySelector('[aria-invalid="true"]');
(firstError as HTMLElement)?.focus();
}
// Skip link
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" tabindex="-1">...</main>
```
### Roving Tabindex
```typescript
// For composite widgets (toolbar, menu, tabs)
function setRovingTabindex(container: HTMLElement, selector: string) {
const items = container.querySelectorAll(selector);
items.forEach((item, index) => {
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
});
container.addEventListener('keydown', (e) => {
const current = Array.from(items).indexOf(document.activeElement as Element);
let next = current;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
next = (current + 1) % items.length;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
next = (current - 1 + items.length) % items.length;
}
if (next !== current) {
items[current].setAttribute('tabindex', '-1');
items[next].setAttribute('tabindex', '0');
(items[next] as HTMLElement).focus();
e.preventDefault();
}
});
}
```
## Testing
### Automated Testing
```bash
# axe-core CLI
npx @axe-core/cli https://localhost:3000
# Lighthouse accessibility audit
npx lighthouse http://localhost:3000 --only-categories=accessibility --output=json
# pa11y
npx pa11y http://localhost:3000
# jest-axe for unit tests
npm install --save-dev jest-axe
```
```typescript
// jest-axe example
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('component is accessible', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
```
```typescript
// Playwright accessibility testing
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('page should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
```
### Manual Testing Checklist
**Keyboard Navigation**
- [ ] All interactive elements reachable via Tab
- [ ] Focus order matches visual order
- [ ] Focus indicator always visible
- [ ] No keyboard traps
- [ ] Escape closes modals/menus
**Screen Reader Testing**
- [ ] VoiceOver (macOS): Cmd+F5
- [ ] NVDA (Windows): Free download
- [ ] Test: Links announce destination
- [ ] Test: Forms announce labels and errors
- [ ] Test: Dynamic content announced
**Visual Testing**
- [ ] Zoom to 200% without horizontal scroll
- [ ] Color contrast meets ratios
- [ ] Information not conveyed by color alone
- [ ] Focus indicators visible in all themes
## Common Fixes
### Missing Accessible Name
```html
<!-- Bad: Icon button without label -->
<button><svg>...</svg></button>
<!-- Good: Add aria-label -->
<button aria-label="Close">
<svg aria-hidden="true">...</svg>
</button>
```
### Missing Form Labels
```html
<!-- Bad: Placeholder as label -->
<input placeholder="Email">
<!-- Good: Proper label -->
<label for="email">Email</label>
<input id="email" type="email">
<!-- Good: Visually hidden label -->
<label for="search" class="visually-hidden">Search</label>
<input id="search" type="search" placeholder="Search...">
```
### Missing Heading Structure
```html
<!-- Bad: Skipping heading levels -->
<h1>Page Title</h1>
<h3>Section</h3> <!-- Missing h2 -->
<!-- Good: Proper hierarchy -->
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
```
### Focus Not Visible
```css
/* Bad: Removing focus outline */
button:focus { outline: none; }
/* Good: Custom focus indicator */
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
```
### Color Contrast
```css
/* Bad: Low contrast */
.text { color: #999; background: #fff; } /* 2.85:1 ratio */
/* Good: Sufficient contrast */
.text { color: #595959; background: #fff; } /* 4.56:1 ratio */
```
## CSS Utilities
```css
/* Visually hidden but accessible */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Skip link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
## Best Practices
### Semantic HTML First
Use native HTML elements before ARIA. A `<button>` is better than `<div role="button">`.
### Don't Override Default Behavior
Native elements have built-in accessibility. Don't break it with JavaScript.
### Test with Real Users
Automated tools catch ~30% of issues. Manual testing with assistive technology is essential.
### Provide Multiple Ways
Offer keyboard, mouse, and touch alternatives for all interactions.
## References
- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/
- ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/
- axe-core Rules: https://dequeuniversity.com/rules/axe/
- A11y Project Checklist: https://www.a11yproject.com/checklist/More Testing skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
setup-pre-commit
mattpocock/skills
Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

