--- 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 ```jsx // ❌ Bad: Divs for everything (no semantic meaning)
Click me
Menu
// ✅ Good: Semantic HTML ``` ### Document Structure ```jsx // ✅ Proper heading hierarchy

Page Title

Section 1

Subsection 1.1

Section 2

// ❌ Bad: Skipping levels

Page Title

Section 1

// Skipped h2, h3 ``` ### Landmarks ```jsx
{/* Main content */}
``` --- ## Pillar 2: Keyboard Navigation ### All Interactive Elements Must Be Keyboard Accessible ```jsx // ✅ Button is keyboard accessible by default // ❌ Div requires extra work
Click me
// Can't tab to it! // ✅ If you must use div, add keyboard support
{ if (e.key === 'Enter' || e.key === ' ') { handleClick() } }} > Click me
``` ### Tab Order ```jsx // ✅ Natural tab order (follows DOM order) Help // ❌ Don't use tabIndex > 0 (breaks natural order) // Anti-pattern! // ✅ tabIndex=-1 to remove from tab order
Not keyboard focusable
``` ### Focus Management ```jsx // Modal: Trap focus inside function Modal({ isOpen, onClose, children }) { const modalRef = useRef() useEffect(() => { if (!isOpen) return // Focus first focusable element const firstFocusable = modalRef.current.querySelector('button, input, a') firstFocusable?.focus() // Trap 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 ? (
{children}
) : null } ``` ### Skip Links ```jsx // Allow keyboard users to skip navigation Skip to main content
{/* Main content */}
// CSS .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 ```jsx // ✅ Semantic HTML (no ARIA needed) // ❌ Unnecessary ARIA // ✅ ARIA needed (custom widget)
Tab 1
``` ### Common ARIA Attributes **aria-label** - Provides accessible name: ```jsx ``` **aria-labelledby** - References another element: ```jsx

Delete Account

{/* Dialog content */}
``` **aria-describedby** - Additional description: ```jsx
Must be at least 8 characters
``` **aria-live** - Announce dynamic content: ```jsx // Polite: Wait for user to finish
{itemsAddedToCart} items added to cart
// Assertive: Interrupt immediately (use sparingly)
Error: Payment failed
``` **aria-expanded** - Collapsible content: ```jsx ``` **aria-hidden** - Hide from screen readers: ```jsx // Decorative icons // Don't hide interactive elements! // ❌ Bad ``` --- ## Pillar 4: Forms & Inputs ### Labels ```jsx // ✅ Good: Explicit label // ✅ Good: Implicit label // ❌ Bad: No label (placeholder is not a label!) ``` ### Error Messages ```jsx function EmailInput({ error }) { const errorId = 'email-error' return ( <> {error && ( )} ) } ``` ### Required Fields ```jsx ``` --- ## 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 ```jsx // ❌ Bad: Insufficient contrast // ✅ Good: Sufficient contrast ``` ### Don't Use Color Alone ```jsx // ❌ Bad: Color only Error Success // ✅ Good: Color + icon/text ``` --- ## Pillar 6: Images & Media ### Alt Text ```jsx // ✅ Informative images Sales increased 50% in Q4 // ✅ Decorative images // Empty alt // ❌ Bad: No alt or redundant alt // Missing alt Photo // Useless ``` ### Complex Images ```jsx
Sales data for 2024
Detailed description

Q1: $100k, Q2: $150k, Q3: $180k, Q4: $220k. Shows 50% growth year-over-year.

``` ### Video Captions ```jsx ``` --- ## Pillar 7: Testing ### Automated Testing ```bash # Lighthouse accessibility audit lighthouse https://example.com --only-categories=accessibility # axe-core (Jest) npm install --save-dev @axe-core/react jest-axe ``` ```typescript // Test with jest-axe import { axe, toHaveNoViolations } from 'jest-axe' expect.extend(toHaveNoViolations) it('has no accessibility violations', async () => { const { container } = render() 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 ```jsx ``` ### Accessible Modal ```jsx

Dialog Title

Dialog description

``` ### Accessible Tabs ```jsx
``` --- ## Accessibility Checklist ### Semantic HTML - [ ] Proper heading hierarchy (h1 → h2 → h3) - [ ] Semantic landmarks (header, nav, main, footer) - [ ] Lists use ul/ol/li - [ ] Buttons for actions, links for navigation ### Keyboard - [ ] All interactive elements keyboard accessible - [ ] Visible focus indicators - [ ] Logical tab order - [ ] Skip links provided - [ ] No keyboard traps ### ARIA - [ ] Semantic HTML used first (ARIA only when needed) - [ ] All interactive widgets have roles - [ ] Dynamic content has aria-live - [ ] Forms have proper labels and descriptions ### Color & Contrast - [ ] Text contrast ≥ 4.5:1 (normal), ≥ 3:1 (large) - [ ] Don't use color alone to convey info - [ ] Focus indicators visible ### Images & Media - [ ] All images have alt text - [ ] Decorative images have empty alt - [ ] Videos have captions - [ ] Audio has transcripts ### Forms - [ ] All inputs have labels - [ ] Error messages associated with inputs - [ ] Required fields indicated ### Testing - [ ] Keyboard navigation tested - [ ] Screen reader tested - [ ] Automated tools run (axe, Lighthouse) - [ ] Color blindness simulation tested --- ## 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:** - [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/) - [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility) - [A11y Project](https://www.a11yproject.com/) - [WebAIM](https://webaim.org/) --- **Build for everyone.** ♿