| name | accessibility-implementation |
| description | This skill should be used when implementing accessible interfaces and ensuring WCAG 2.1 compliance - covers ARIA labels, keyboard navigation, screen reader support, color contrast, focus management, and semantic HTML for inclusive design that works for all users. |
Accessibility Implementation
Overview
Build interfaces that work for everyone, including users with disabilities. Meet WCAG 2.1 Level AA standards through systematic accessibility practices.
Core principle: Accessibility is not optional - it's a fundamental requirement for quality software.
When to Use
Use when:
- Building new UI components
- Required to meet WCAG compliance
- Users report accessibility issues
- Implementing forms, navigation, or interactive elements
- Before launching to production
- Working with accessibility-specialist agent
WCAG 2.1 Level AA Essentials
1. Perceivable
Color Contrast:
- Normal text: 4.5:1 minimum
- Large text (18pt+): 3:1 minimum
- UI components: 3:1 minimum
Check contrast:
Tool: WebAIM Contrast Checker
https://webaim.org/resources/contrastchecker/
Don't rely on color alone:
❌ Red/green for error/success only
✅ Red + icon + text: "Error: Invalid email"
Alt text for images:
<img src="chart.png" alt="Revenue up 23% vs last month" />
// Not: alt="chart"
// Not: alt="" (unless purely decorative)
2. Operable
Keyboard navigation:
- All interactive elements focusable
- Tab order makes sense
- Enter/Space activate buttons
- Escape closes modals
- Arrow keys for menus/selects
Focus indicators:
button:focus-visible {
outline: 2px solid #3B82F6;
outline-offset: 2px;
}
/* Don't remove outlines! */
❌ *:focus {outline: none}
Skip links:
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<main id="main-content">
{/* Page content */}
</main>
.skip-link {
position: absolute;
top: -40px;
left: 0;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
3. Understandable
Clear labels:
❌ <input placeholder="Enter text" />
✅ <label htmlFor="email">Email address</label>
<input id="email" type="email" />
Error messages:
<input
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && (
<p id="email-error" role="alert">
Please enter a valid email address
</p>
)}
4. Robust
Semantic HTML:
✅ <button onClick={handleClick}>Click me</button>
❌ <div onClick={handleClick}>Click me</div>
✅ <nav><ul><li><a href="/about">About</a></li></ul></nav>
❌ <div><div><div onClick={navigate}>About</div></div></div>
ARIA Patterns
Buttons
<button
type="button"
aria-label="Close dialog"
onClick={onClose}
>
<X /> {/* Icon only */}
</button>
Links vs Buttons
Use <button> for actions (submit, open modal, toggle)
Use <a> for navigation (go to different page)
✅ <button onClick={openModal}>View Details</button>
❌ <a onClick={openModal}>View Details</a>
✅ <a href="/about">About Us</a>
❌ <button onClick={() => navigate('/about')}>About Us</button>
Form Controls
<label htmlFor="name">Full Name</label>
<input
id="name"
type="text"
required
aria-required="true"
aria-describedby="name-help"
/>
<p id="name-help">Enter your first and last name</p>
Custom Components
// Custom checkbox
<div
role="checkbox"
aria-checked={checked}
aria-label="Accept terms"
tabIndex={0}
onClick={toggle}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') toggle()
}}
>
{checked ? <CheckIcon /> : <UncheckedIcon />}
</div>
Modals/Dialogs
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Delete</h2>
<p id="dialog-description">
This action cannot be undone.
</p>
<button onClick={onDelete}>Delete</button>
<button onClick={onCancel}>Cancel</button>
</div>
Focus management:
useEffect(() => {
if (isOpen) {
// Focus first element
const firstFocusable = dialogRef.current?.querySelector('button, [href], input')
firstFocusable?.focus()
// Trap focus inside dialog
// Return focus on close
}
}, [isOpen])
Keyboard Navigation
Standard Patterns
| Element | Keys | Behavior |
|---|---|---|
| Button | Enter, Space | Activate |
| Link | Enter | Navigate |
| Checkbox | Space | Toggle |
| Radio | Arrow keys | Select option |
| Select | Arrow keys, Enter | Navigate, select |
| Tab | Tab | Next focusable |
| Tab | Shift+Tab | Previous focusable |
| Modal | Escape | Close |
| Menu | Arrow keys, Escape | Navigate, close |
Implementation
function Button({onClick, children}: ButtonProps) {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onClick()
}
}
return (
<button
onClick={onClick}
onKeyDown={handleKeyDown}
type="button"
>
{children}
</button>
)
}
Screen Reader Support
Announce Dynamic Content
// Live regions for updates
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// aria-live="polite": Announce when user is idle
// aria-live="assertive": Announce immediately (errors)
// aria-atomic="true": Read entire region, not just changes
Visually Hidden Text
// Text for screen readers only
<span className="sr-only">
Loading...
</span>
<Spinner aria-hidden="true" />
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
Loading States
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? (
<>
<Spinner aria-hidden="true" />
<span className="sr-only">Loading...</span>
</>
) : (
'Submit'
)}
</button>
Testing for Accessibility
Automated Testing
# Install axe
npm install --save-dev @axe-core/react
# Add to app (development only)
if (process.env.NODE_ENV !== 'production') {
import('@axe-core/react').then(axe => {
axe.default(React, ReactDOM, 1000)
})
}
Manual Testing
Keyboard testing:
- Tab through entire page
- Activate all interactive elements with Enter/Space
- Escape closes modals
- Focus visible at all times
- Tab order logical
Screen reader testing:
- Mac: VoiceOver (Cmd+F5)
- Windows: NVDA (free)
- Test critical flows (signup, checkout)
Lighthouse Audit
# Run Lighthouse in Chrome DevTools
# Accessibility score should be 90+
shadcn/ui Accessibility
Built-in accessibility:
- All components keyboard navigable
- ARIA attributes included
- Focus management handled
- Semantic HTML
Still required:
- Meaningful labels
- Error messages
- Proper heading hierarchy
- Sufficient color contrast
Example:
import {Button} from "@/components/ui/button"
// Already accessible:
// - Keyboard navigable
// - Focus styles
// - Proper semantics
// You add:
<Button aria-label="Delete item">
<Trash2 />
</Button>
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Missing alt text | Images invisible to screen readers | Add descriptive alt text |
| Div buttons | Not keyboard accessible | Use <button> element |
| No focus indicators | Can't see where you are | Add :focus-visible styles |
| Color only for info | Color blind users miss info | Add text/icons too |
| Automatic media | Unexpected sound/movement | Require user interaction |
| Missing labels | Forms unusable | Label all inputs |
| Poor heading structure | Can't navigate by headings | Use h1-h6 properly |
Quick Wins
Immediate improvements:
- Add alt text to all images
- Use semantic HTML (button, nav, header, main)
- Label all form inputs
- Ensure keyboard navigation works
- Add focus indicators
- Check color contrast
- Test with keyboard only
Expected: 90+ Lighthouse score in 1 day
Resources
- WCAG 2.1: w3.org/WAI/WCAG21/quickref
- ARIA patterns: w3.org/WAI/ARIA/apg
- Contrast checker: webaim.org/resources/contrastchecker
- axe DevTools: chrome extension
Accessibility isn't extra work - it's building products correctly. Start with semantic HTML, add ARIA when needed, test systematically.