Accessibility¶
Building an inclusive web experience for everyone.
WCAG Compliance¶
This site meets WCAG 2.1 AA standards across all pages.
Conformance Levels¶
- ✅ Level A: All criteria met
- ✅ Level AA: All criteria met
- ⚠️ Level AAA: Partial (where applicable)
Key Features¶
1. Semantic HTML¶
Proper HTML structure for assistive technologies:
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<!-- Footer content -->
</footer>
2. Keyboard Navigation¶
Full site navigation via keyboard:
- Tab: Move forward through interactive elements
- Shift + Tab: Move backward
- Enter/Space: Activate buttons and links
- Escape: Close modals and menus
Focus Indicators:
3. Screen Reader Support¶
ARIA Labels:
<button
aria-label="Toggle dark mode"
aria-pressed={isDark}
>
<span aria-hidden="true">🌙</span>
</button>
Live Regions:
Skip Links:
4. Color and Contrast¶
Contrast Ratios:
- Normal text: 7:1 (AAA)
- Large text: 4.5:1 (AA)
- Interactive elements: 3:1 (AA)
Color Independence:
Information never conveyed by color alone:
<!-- Bad -->
<span style="color: red;">Error</span>
<!-- Good -->
<span class="error">
<span aria-label="Error">⚠️</span>
Error message
</span>
5. Motion and Animation¶
Respects reduced motion preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
6. Form Accessibility¶
Proper labels and error handling:
<label for="email">
Email Address
<span aria-label="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
aria-required="true"
aria-invalid={hasError}
aria-describedby="email-error"
>
<span id="email-error" role="alert">
{errorMessage}
</span>
Testing¶
Automated Testing¶
axe-core Integration:
import { axe } from 'axe-core';
describe('Accessibility', () => {
it('has no violations', async () => {
const results = await axe(document.body);
expect(results.violations).toHaveLength(0);
});
});
Manual Testing¶
Screen Readers: - ✅ NVDA (Windows) - ✅ JAWS (Windows) - ✅ VoiceOver (macOS/iOS) - ✅ TalkBack (Android)
Keyboard Only: - ✅ All interactive elements reachable - ✅ Logical tab order - ✅ Visible focus indicators
Browser Testing¶
Tested across: - Chrome/Edge with ChromeVox - Firefox with NVDA - Safari with VoiceOver
Common Patterns¶
Interactive Buttons¶
interface ButtonProps {
onClick: () => void;
ariaLabel?: string;
disabled?: boolean;
}
export default function Button({
onClick,
ariaLabel,
disabled
}: ButtonProps) {
return (
<button
onClick={onClick}
aria-label={ariaLabel}
disabled={disabled}
aria-disabled={disabled}
>
Click me
</button>
);
}
Expandable Sections¶
<button
aria-expanded={isOpen}
aria-controls="section-content"
onclick="toggleSection()"
>
Toggle Section
</button>
<div
id="section-content"
hidden={!isOpen}
>
Content here
</div>
Modal Dialogs¶
export default function Modal({ isOpen, onClose }) {
useEffect(() => {
if (isOpen) {
// Trap focus
// Set focus to first element
}
}, [isOpen]);
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
hidden={!isOpen}
>
<h2 id="modal-title">Modal Title</h2>
<button onClick={onClose}>Close</button>
</div>
);
}
Best Practices¶
Images¶
Decorative images:
Informative images:
Links¶
Descriptive link text:
<!-- Bad -->
<a href="/docs">Click here</a>
<!-- Good -->
<a href="/docs">Read the documentation</a>
Headings¶
Logical hierarchy: