Web Accessibility Standards
Core Principles (WCAG 2.2)
All code must follow WCAG 2.2 Level AA standards:
- Perceivable - Information must be presentable to all users
- Operable - UI components must be operable by all users
- Understandable - Information and operation must be clear
- Robust - Content must work with current and future technologies
Essential Requirements
1. Semantic HTML First
Always use native HTML elements before custom implementations:
tsx1// ✅ GOOD: Native semantics 2<button onClick={handleClick}>Submit</button> 3<a href="/page">Navigate</a> 4<input type="checkbox" /> 5 6// ❌ BAD: Custom elements without semantics 7<div onClick={handleClick}>Submit</div> 8<span onClick={navigate}>Navigate</span>
2. Text Alternatives
All non-text content must have text alternatives:
tsx1// ✅ GOOD: Descriptive alt text 2<img src="/chart.png" alt="Sales increased 25% in Q4 2024" /> 3 4// ✅ GOOD: Empty alt for decorative images 5<img src="/decoration.png" alt="" /> 6 7// ✅ GOOD: Icon buttons with labels 8<button aria-label="Close dialog"> 9 <IconClose aria-hidden="true" /> 10</button> 11 12// ❌ BAD: Missing alt text 13<img src="/chart.png" />
3. Keyboard Accessibility
All interactive elements must be keyboard accessible:
Required keyboard support:
Tab/Shift+Tab- Navigate between elementsEnter- Activate buttons/linksSpace- Activate buttons/checkboxesEscape- Close modals/dialogs- Arrow keys - Navigate menus/tabs/lists
tsx1// ✅ GOOD: Keyboard accessible 2<button onClick={handleClick}>Click me</button> 3 4// ❌ BAD: Not keyboard accessible 5<div onClick={handleClick}>Click me</div>
4. Visible Focus Indicators
All interactive elements must have visible focus indicators:
css1/* ✅ GOOD: Visible focus indicator */ 2button:focus-visible { 3 outline: 2px solid #0066CC; 4 outline-offset: 2px; 5} 6 7/* ❌ BAD: Removing focus outline */ 8button:focus { 9 outline: none; 10}
5. Color and Contrast
WCAG AA Requirements:
- Normal text (< 18pt): 4.5:1 minimum
- Large text (≥ 18pt or bold 14pt): 3:1 minimum
- UI components: 3:1 minimum
tsx1// ❌ BAD: Color is only indicator 2<span style={{ color: 'red' }}>Error occurred</span> 3 4// ✅ GOOD: Icon + text + color 5<span style={{ color: 'red' }}> 6 <IconError aria-hidden="true" /> 7 Error: Invalid email address 8</span>
6. Form Accessibility
All form inputs must have associated labels:
tsx1// ✅ GOOD: Proper form structure 2<label htmlFor="email"> 3 Email Address 4 <span aria-label="required">*</span> 5</label> 6<input 7 id="email" 8 type="email" 9 required 10 aria-required="true" 11 aria-invalid={!!error} 12 aria-describedby={error ? "email-error" : undefined} 13/> 14{error && ( 15 <div id="email-error" role="alert"> 16 {error} 17 </div> 18)} 19 20// ❌ BAD: Placeholder as label 21<input type="email" placeholder="Email Address" />
7. Heading Hierarchy
Use proper heading levels without skipping:
tsx1// ✅ GOOD: Logical hierarchy 2<h1>Page Title</h1> 3 <h2>Section 1</h2> 4 <h3>Subsection 1.1</h3> 5 <h2>Section 2</h2> 6 7// ❌ BAD: Skipping levels 8<h1>Page Title</h1> 9<h4>Section</h4> // Skipped h2, h3
8. ARIA Usage
Use ARIA only when native HTML is insufficient:
tsx1// Common ARIA patterns 2<button 3 aria-expanded={isOpen} 4 aria-controls="menu-id" 5> 6 Menu 7</button> 8 9<div 10 role="dialog" 11 aria-modal="true" 12 aria-labelledby="dialog-title" 13> 14 <h2 id="dialog-title">Dialog Title</h2> 15</div> 16 17<div role="alert">Critical error occurred</div> 18<div role="status" aria-live="polite">Loading complete</div>
9. Focus Management
Manage focus for dynamic content:
tsx1// ✅ GOOD: Modal with focus management 2const Modal = ({ isOpen, onClose, title }) => { 3 const modalRef = useRef<HTMLDivElement>(null); 4 5 useEffect(() => { 6 if (!isOpen) return; 7 8 const previousFocus = document.activeElement; 9 modalRef.current?.querySelector('button')?.focus(); 10 11 const handleEscape = (e: KeyboardEvent) => { 12 if (e.key === 'Escape') onClose(); 13 }; 14 15 document.addEventListener('keydown', handleEscape); 16 17 return () => { 18 document.removeEventListener('keydown', handleEscape); 19 (previousFocus as HTMLElement)?.focus(); 20 }; 21 }, [isOpen, onClose]); 22 23 return ( 24 <div ref={modalRef} role="dialog" aria-modal="true"> 25 <h2>{title}</h2> 26 <button onClick={onClose}>Close</button> 27 </div> 28 ); 29};
10. Dynamic Content Announcements
Announce dynamic content changes to screen readers:
tsx1// Status messages (polite - wait for user) 2<div role="status" aria-live="polite"> 3 {statusMessage} 4</div> 5 6// Error alerts (assertive - interrupt) 7<div role="alert" aria-live="assertive"> 8 {errorMessage} 9</div>
Common Patterns
Screen Reader Only Text
tsx1const srOnly: React.CSSProperties = { 2 position: 'absolute', 3 width: '1px', 4 height: '1px', 5 padding: '0', 6 margin: '-1px', 7 overflow: 'hidden', 8 clip: 'rect(0, 0, 0, 0)', 9 whiteSpace: 'nowrap', 10 border: '0', 11}; 12 13<button> 14 <IconTrash /> 15 <span style={srOnly}>Delete item</span> 16</button>
Skip Links
tsx1<a href="#main-content" className="skip-link"> 2 Skip to main content 3</a> 4<header>...</header> 5<main id="main-content">...</main>
Accessible Tables
tsx1<table> 2 <caption>Sales Data Q4 2024</caption> 3 <thead> 4 <tr> 5 <th scope="col">Month</th> 6 <th scope="col">Revenue</th> 7 </tr> 8 </thead> 9 <tbody> 10 <tr> 11 <th scope="row">October</th> 12 <td>$50,000</td> 13 </tr> 14 </tbody> 15</table>
Top 10 Mistakes to Avoid
- Missing alt text on images
- Using
<div>or<span>instead of<button>for actions - Missing form labels (using placeholder instead)
- Removing focus outline without replacement
- Skipping heading levels (h1 → h4)
- Insufficient color contrast (< 4.5:1)
- Color as only indicator (no icon/text)
- No keyboard access to interactive elements
- Not announcing dynamic content changes
- Icon buttons without labels or aria-label
Testing Checklist
Automated Testing
- Use axe-core, Lighthouse, or WAVE browser extensions
- Add automated tests with jest-axe or similar tools
Manual Testing
- Tab through all interactive elements
- All elements reachable by keyboard
- Focus visible on all elements
- Enter/Space activates buttons
- Escape closes modals
- Works at 200% zoom
- Color contrast meets WCAG AA
- Test with screen reader (VoiceOver/NVDA/JAWS)
Resources
- WCAG 2.2 Guidelines: https://www.w3.org/TR/WCAG22/
- WCAG Quick Reference: https://www.w3.org/WAI/WCAG22/quickref/
- ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/
- WebAIM: https://webaim.org/
- MDN Accessibility: https://developer.mozilla.org/en-US/docs/Web/Accessibility
Best Practices Summary
✅ Always Do
- Use semantic HTML first
- Provide text alternatives
- Ensure keyboard accessibility
- Maintain sufficient color contrast
- Manage focus for dynamic content
- Use proper heading hierarchy
- Label all form inputs
- Test with assistive technology
❌ Never Do
- Remove focus outline without replacement
- Use color as only indicator
- Create keyboard traps
- Skip heading levels
- Use placeholder as label
- Make non-interactive elements clickable
- Rely solely on automated testing
- Add ARIA when native HTML works