Web Accessibility (a11y)
Why Accessibility?
Over 1 billion people worldwide have disabilities. Accessible websites work for everyone — including users with screen readers, keyboard-only navigation, color blindness, or motor impairments. Many countries require accessibility by law (ADA, EN 301 549, AODA).
Accessible design also improves SEO, mobile UX, and code quality.
WCAG Guidelines
The Web Content Accessibility Guidelines (WCAG) 2.2 define three conformance levels:
| Level | Target |
|---|---|
| A | Minimum — legal baseline in some jurisdictions |
| AA | Industry standard — most organizations aim here |
| AAA | Enhanced — not always feasible for all content |
Key principles (POUR):
- Perceivable — content available to all senses (text alternatives, contrast, captions)
- Operable — keyboard accessible, enough time, no seizure triggers
- Understandable — readable, predictable, helpful error messages
- Robust — valid markup, compatible with assistive technology
Essential Practices
Alt Text for Images
<img src="chart.png" alt="Sales increased 40% from Q1 to Q2 2024">
<img src="decorative-line.png" alt="" role="presentation">
- Informative images: Describe the content and function
- Decorative images: Empty
alt=""so screen readers skip them - Complex images: Link to a long description or use
<figcaption>
Keyboard Navigation
All interactive elements must be reachable and operable via keyboard:
<button>Click me</button> <!-- naturally focusable -->
<a href="/page">Link</a> <!-- naturally focusable -->
<!-- Custom controls need tabindex and keyboard handlers -->
<div role="button" tabindex="0"
onclick="activate()"
onkeydown="if(event.key==='Enter'||event.key===' ') activate()">
Custom button
</div>
Focus order should follow visual order. Use :focus-visible for clear focus rings:
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
Skip Links
<a href="#main" class="skip-link">Skip to main content</a>
<main id="main">...</main>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
z-index: 1000;
}
Form Labels and Errors
<label for="email">Email address</label>
<input type="email" id="email" name="email" required
aria-describedby="email-hint email-error">
<span id="email-hint">We'll never share your email.</span>
<span id="email-error" role="alert" hidden>Please enter a valid email.</span>
Never rely on placeholder text alone as a label.
Color Contrast
Text must meet contrast ratios:
- Normal text: 4.5:1 minimum (AA)
- Large text (18px+ or 14px+ bold): 3:1 minimum
Tools: WebAIM Contrast Checker, Lighthouse, axe.
Don’t convey information by color alone — add icons, text, or patterns.
ARIA When Needed
<nav aria-label="Breadcrumb">
<button aria-expanded="false" aria-controls="menu" id="menuBtn">Menu</button>
<ul id="menu" aria-labelledby="menuBtn" hidden>...</ul>
<div role="alert">Form submitted successfully!</div>
First rule of ARIA: No ARIA is better than bad ARIA. Prefer native HTML elements first.
Common ARIA patterns:
aria-expanded/aria-controlsfor disclosure menusaria-live="polite"for dynamic updatesaria-hidden="true"on decorative icons next to visible text
Headings and Landmarks
- One
<h1>per page - Don’t skip heading levels (
h1→h2→h3) - Use landmark elements:
<header>,<nav>,<main>,<footer>
Screen reader users often navigate by headings or landmarks — broken hierarchy disorients them.
Testing Accessibility
| Method | Tools |
|---|---|
| Automated | axe DevTools, Lighthouse, WAVE |
| Manual keyboard | Tab through entire page; no keyboard traps |
| Screen reader | VoiceOver (macOS), NVDA (Windows), TalkBack (Android) |
| Zoom | Test at 200% zoom — content should reflow, not clip |
Automated tools catch ~30% of issues — manual testing is essential.
Common Mistakes
| Mistake | Fix |
|---|---|
<div onclick> without keyboard support |
Use <button> or add tabindex + key handlers |
| Missing form labels | Associate <label for="id"> with every input |
| Low contrast text | Check with contrast checker |
| Autoplaying media | Provide controls; respect prefers-reduced-motion |
Troubleshooting
Focus disappears after closing modal
- Return focus to the trigger element with
trigger.focus()
Screen reader reads “clickable” on non-interactive element
- Remove
role="button"or add proper keyboard behavior
Lighthouse accessibility score low
- Fix color contrast and missing alt text first — highest impact
Accessibility is not optional — it’s a core skill for professional web developers and a legal requirement in many contexts.