| name | Accessibility Engineer |
| description | Implement accessibility (a11y) best practices to make applications usable by everyone. Use when building UIs, conducting accessibility audits, or ensuring WCAG compliance. Covers screen readers, keyboard navigation, ARIA attributes, and inclusive design patterns. |
| version | 1.0.0 |
Accessibility Engineer
Build for everyone - accessibility is not optional.
Core Principle
Accessibility is a civil right, not a feature.
1 in 4 adults in the US has a disability. Accessible design benefits everyone:
- Blind users (screen readers)
- Low vision users (zoom, high contrast)
- Deaf users (captions)
- Motor disabilities (keyboard-only)
- Cognitive disabilities (clear language)
- Temporary disabilities (broken arm)
- Situational limitations (bright sunlight, noisy environment)
WCAG Compliance Levels
Level A: Minimum (legal requirement)
Level AA: Industry standard (aim for this)
Level AAA: Gold standard (difficult to achieve for all content)
Target: WCAG 2.1 AA compliance
Pillar 1: Semantic HTML
Use the Right Elements
<div onClick={handleClick}>Click me</div>
<div>Menu</div>
<button onClick={handleClick}>Click me</button>
<nav>Menu</nav>
Document Structure
<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h2>Section 2</h2>
<h1>Page Title</h1>
<h4>Section 1</h4>
Landmarks
<header>
<nav aria-label="Main navigation">
{/* Navigation links */}
</nav>
</header>
<main>
<article>
{/* Main content */}
</article>
<aside>
{/* Sidebar */}
</aside>
</main>
<footer>
{/* Footer content */}
</footer>
Pillar 2: Keyboard Navigation
All Interactive Elements Must Be Keyboard Accessible
<button onClick={handleClick}>Click me</button>
<div onClick={handleClick}>Click me</div>
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
Tab Order
<input />
<button>Submit</button>
<a href="/help">Help</a>
<button tabIndex={5}>Button</button>
<div tabIndex={-1}>Not keyboard focusable</div>
Focus Management
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef()
useEffect(() => {
if (!isOpen) return
const firstFocusable = modalRef.current.querySelector('button, input, a')
firstFocusable?.focus()
function handleTab(e) {
if (e.key !== 'Tab') return
const focusableElements = modalRef.current.querySelectorAll(
'button, input, a, [tabindex]:not([tabindex="-1"])'
)
const first = focusableElements[0]
const last = focusableElements[focusableElements.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
last.focus()
e.preventDefault()
}
} else {
if (document.activeElement === last) {
first.focus()
e.preventDefault()
}
}
}
document.addEventListener('keydown', handleTab)
return () => document.removeEventListener('keydown', handleTab)
}, [isOpen])
return isOpen ? (
<div role="dialog" aria-modal="true" ref={modalRef}>
{children}
<button onClick={onClose}>Close</button>
</div>
) : null
}
Skip Links
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<nav>{/* Navigation */}</nav>
<main id="main-content">
{/* Main content */}
</main>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
}
.skip-link:focus {
top: 0;
}
Pillar 3: ARIA Attributes
Only Use ARIA When Semantic HTML Isn't Enough
<button>Click me</button>
<button role="button" aria-label="Click me">Click me</button>
<div role="tab" aria-selected={isActive} aria-controls="panel-1">
Tab 1
</div>
Common ARIA Attributes
aria-label - Provides accessible name:
<button aria-label="Close dialog">
<XIcon /> {}
</button>
<input type="search" aria-label="Search products" />
aria-labelledby - References another element:
<h2 id="dialog-title">Delete Account</h2>
<div role="dialog" aria-labelledby="dialog-title">
{/* Dialog content */}
</div>
aria-describedby - Additional description:
<input
type="password"
aria-describedby="password-requirements"
/>
<div id="password-requirements">
Must be at least 8 characters
</div>
aria-live - Announce dynamic content:
<div aria-live="polite">
{itemsAddedToCart} items added to cart
</div>
<div aria-live="assertive" role="alert">
Error: Payment failed
</div>
aria-expanded - Collapsible content:
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
<div id="dropdown-menu" hidden={!isOpen}>
{/* Menu items */}
</div>
aria-hidden - Hide from screen readers:
<span aria-hidden="true">★</span>
<button aria-hidden="true">Click me</button>
Pillar 4: Forms & Inputs
Labels
<label htmlFor="email">Email</label>
<input id="email" type="email" />
<label>
Email
<input type="email" />
</label>
<input type="email" placeholder="Email" />
Error Messages
function EmailInput({ error }) {
const errorId = 'email-error'
return (
<>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
/>
{error && (
<div id={errorId} role="alert">
{error}
</div>
)}
</>
)
}
Required Fields
<label htmlFor="name">
Name <span aria-label="required">*</span>
</label>
<input id="name" type="text" required aria-required="true" />
Pillar 5: Color & Contrast
Minimum Contrast Ratios (WCAG AA)
- Normal text (< 18pt): 4.5:1
- Large text (≥ 18pt or 14pt bold): 3:1
- UI components: 3:1
<button style={{ background: '#ddd', color: '#aaa' }}>
Submit
</button>
<button style={{ background: '#0066cc', color: '#ffffff' }}>
Submit // 8:1 contrast - passes!
</button>
Don't Use Color Alone
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
Error
</span>
Pillar 6: Images & Media
Alt Text
<img src="chart.png" alt="Sales increased 50% in Q4" />
<img src="decorative-border.png" alt="" />
<img src="photo.jpg" />
<img src="photo.jpg" alt="Photo" />
Complex Images
<figure>
<img src="complex-chart.png" alt="Sales data for 2024" />
<figcaption>
<details>
<summary>Detailed description</summary>
<p>Q1: $100k, Q2: $150k, Q3: $180k, Q4: $220k. Shows 50% growth year-over-year.</p>
</details>
</figcaption>
</figure>
Video Captions
<video controls>
<source src="video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" default />
</video>
Pillar 7: Testing
Automated Testing
lighthouse https://example.com --only-categories=accessibility
npm install --save-dev @axe-core/react jest-axe
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Manual Testing
Keyboard Navigation:
- Tab through entire page
- Enter/Space to activate buttons
- Arrow keys for radio groups
- Esc to close modals
Screen Reader Testing:
- NVDA (Windows, free)
- JAWS (Windows, paid)
- VoiceOver (Mac, built-in)
Screen Reader Shortcuts:
- Navigate by headings: H (next), Shift+H (previous)
- Navigate by landmarks: D (next), Shift+D (previous)
- List all links: Insert+F7 (NVDA)
Common Patterns
Accessible Button
<button
type="button"
onClick={handleClick}
disabled={isDisabled}
aria-busy={isLoading}
aria-label={ariaLabel}
>
{children}
</button>
Accessible Modal
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<p id="dialog-description">Dialog description</p>
<button onClick={onClose}>Close</button>
</div>
Accessible Tabs
<div>
<div role="tablist">
<button
role="tab"
aria-selected={activeTab === 'tab1'}
aria-controls="panel1"
onClick={() => setActiveTab('tab1')}
>
Tab 1
</button>
<button
role="tab"
aria-selected={activeTab === 'tab2'}
aria-controls="panel2"
onClick={() => setActiveTab('tab2')}
>
Tab 2
</button>
</div>
<div id="panel1" role="tabpanel" hidden={activeTab !== 'tab1'}>
Panel 1 content
</div>
<div id="panel2" role="tabpanel" hidden={activeTab !== 'tab2'}>
Panel 2 content
</div>
</div>
Accessibility Checklist
Semantic HTML
Keyboard
ARIA
Color & Contrast
Images & Media
Forms
Testing
Tools
- axe DevTools - Browser extension
- Lighthouse - Built into Chrome DevTools
- WAVE - Web accessibility evaluation tool
- Color Contrast Analyzer - Desktop app
- NVDA - Free screen reader (Windows)
- VoiceOver - Built-in screen reader (Mac)
Related Resources
Skills:
ux-designer - Accessible design patterns
frontend-builder - Accessible React components
testing-strategist - Accessibility testing
External:
Build for everyone. ♿