Why Accessibility Matters
Over 1 billion people worldwide live with some form of disability. Web accessibility isn't just about compliance — it's about not locking people out of your product. It also improves SEO, usability for all users, and in many jurisdictions, it's legally required.
The Four WCAG Principles (POUR)
Perceivable — Users can perceive all information Operable — Users can operate the interface Understandable — Users understand content and UI Robust — Works with current and future assistive technologies
WCAG 2.1 Level AA is the standard target for most web applications.
1. Semantic HTML
Use the right HTML element for the job. Screen readers announce the element's role:
<!-- Bad -->
<div onclick="submit()">Submit</div>
<div class="header">My Site</div>
<div class="nav">...</div>
<!-- Good -->
<button type="submit">Submit</button>
<header><h1>My Site</h1></header>
<nav aria-label="Main navigation">...</nav>
Semantic elements provide keyboard interaction and ARIA roles for free.
2. Image Alt Text
<!-- Informative image -->
<img src="chart.png" alt="Bar chart showing 40% increase in revenue Q1 2025" />
<!-- Decorative image — empty alt tells screen readers to skip it -->
<img src="divider.svg" alt="" role="presentation" />
<!-- Icon button — describe the action -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
3. Keyboard Navigation
All interactive elements must be reachable and operable with a keyboard alone:
<!-- Natural tab order — don't use tabindex > 0 -->
<button>First</button>
<button>Second</button>
<button>Third</button>
<!-- Custom focusable element -->
<div role="button" tabindex="0" onkeydown="handleKey(event)">
Click me
</div>
Test by pressing Tab through your entire UI. Every interactive element should be reachable, and focus should be clearly visible.
4. Focus Management
When dialogs open, move focus inside them. When they close, return focus to the trigger:
function Dialog({ isOpen, onClose, triggerRef }) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
dialogRef.current?.focus();
} else {
triggerRef.current?.focus();
}
}, [isOpen]);
return isOpen ? (
<div role="dialog" aria-modal="true" ref={dialogRef} tabIndex={-1}>
{/* content */}
</div>
) : null;
}
5. Color Contrast
WCAG AA requires:
- 4.5:1 contrast ratio for normal text
- 3:1 for large text (18pt+ or 14pt+ bold)
Tools:
- WebAIM Contrast Checker
- Chrome DevTools → CSS overview → Colors
- Figma plugins: Contrast, A11y Annotation Kit
Never convey information by color alone — add icons, patterns, or text labels.
6. ARIA Labels and Descriptions
<!-- Label a landmark region -->
<nav aria-label="Breadcrumb">...</nav>
<!-- Describe an input more fully -->
<input
type="password"
aria-describedby="pwd-hint"
/>
<p id="pwd-hint">Must be 8+ characters with one number</p>
<!-- Live regions for dynamic updates -->
<div aria-live="polite" aria-atomic="true">
Form submitted successfully!
</div>
7. Forms
<!-- Associate every input with a label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" required autocomplete="email" />
<!-- Group related inputs -->
<fieldset>
<legend>Shipping address</legend>
<!-- inputs -->
</fieldset>
<!-- Error messages -->
<input
type="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<span id="email-error" role="alert">
Please enter a valid email address.
</span>
Testing Accessibility
- Keyboard only — unplug your mouse and try using your app
- Screen reader — VoiceOver (Mac/iOS), NVDA (Windows), TalkBack (Android)
- Automated tools — axe DevTools (browser extension), Lighthouse audit
- Color blindness simulators — Chrome DevTools → Rendering → Emulate vision deficiencies
Conclusion
Start with semantic HTML and you'll get 60% of accessibility for free. Add proper labels, keyboard navigation, and visible focus states for another 30%. The remaining 10% is nuanced ARIA patterns for complex widgets. Automated tools catch about 30-40% of issues — manual testing with a keyboard and screen reader is irreplaceable.