Accessibility Standards
Accessibility is not optional—it's a fundamental requirement for every component in your design system. Accessible components ensure that all users, regardless of ability, can effectively use your products. These standards provide the foundation for creating inclusive, compliant components.
Why Accessibility Matters
Accessible components:
- Legal Compliance: Meet WCAG 2.1 Level AA requirements
- User Inclusion: Serve users with disabilities
- Better UX: Improve experience for all users
- Business Value: Expand your user base
Core Requirements
1. Semantic HTML
Use semantic HTML elements that convey meaning:
// ❌ Bad: Generic elements
<div onClick={handleClick}>Click me</div>
<span role="button">Submit</span>
// ✅ Good: Semantic elements
<button onClick={handleClick}>Click me</button>
<button type="submit">Submit</button>2. Keyboard Navigation
All interactive elements must be keyboard accessible:
// ✅ Good: Keyboard accessible
<button
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
}}
>
Click me
</button>
// Native button already handles keyboard - no extra code needed!
<button onClick={handleClick}>Click me</button>3. ARIA Attributes
Use ARIA when HTML semantics aren't sufficient:
// ✅ Good: ARIA for complex components
<div
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">Are you sure you want to continue?</p>
</div>
// ✅ Good: ARIA for dynamic content
<div aria-live="polite" aria-atomic="true">
{loading ? 'Loading...' : 'Content loaded'}
</div>4. Color Contrast
Ensure sufficient color contrast for text:
- Normal text: 4.5:1 contrast ratio (WCAG AA)
- Large text: 3:1 contrast ratio (WCAG AA)
- UI components: 3:1 contrast ratio
5. Focus Management
Ensure focus is visible and properly managed:
// ✅ Good: Visible focus indicator
.button:focus-visible {
outline: 2px solid var(--semantic-color-border-focus);
outline-offset: 2px;
}
// ✅ Good: Focus trapping in modals
function Modal({ isOpen, onClose, children }) {
useEffect(() => {
if (isOpen) {
// Trap focus inside modal
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleTab = (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
firstElement?.focus();
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}
}, [isOpen]);
return <div ref={modalRef}>{children}</div>;
}Component-Specific Guidelines
Buttons
- Use semantic
<button>element - Provide accessible label (text or aria-label)
- Indicate loading state with aria-busy
- Minimum 44x44px touch target
Form Controls
- Associate labels with inputs using htmlFor/id
- Provide error messages with aria-describedby
- Use aria-required for required fields
- Announce validation errors to screen readers
Dialogs/Modals
- Use role="dialog" or <dialog> element
- Provide aria-labelledby for title
- Trap focus inside modal
- Return focus to trigger when closed
Testing Requirements
Automated Testing
Run automated accessibility tests:
// jest-axe for unit tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('should have no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});Manual Testing
Test with assistive technologies:
- Screen readers: VoiceOver (macOS/iOS), NVDA/JAWS (Windows)
- Keyboard only: Navigate without mouse
- Zoom: Test at 200% zoom
- High contrast: Test in high contrast mode
Common Pitfalls
1. Missing Labels
// ❌ Bad: No label
<input type="text" />
// ✅ Good: Associated label
<label htmlFor="email">Email</label>
<input id="email" type="email" />2. Keyboard Traps
// ❌ Bad: Focus can't escape
<div onKeyDown={(e) => e.preventDefault()}>
{/* Focus trapped */}
</div>
// ✅ Good: Proper focus management
<div
onKeyDown={(e) => {
if (e.key === 'Escape') {
onClose();
}
}}
>
{/* Focus can escape */}
</div>3. Insufficient Color Contrast
// ❌ Bad: Low contrast
.button {
background: #ccc;
color: #ddd; /* Contrast ratio: 1.2:1 */
}
// ✅ Good: Sufficient contrast
.button {
background: #0066cc;
color: #ffffff; /* Contrast ratio: 4.5:1 */
}4. Missing ARIA Labels
// ❌ Bad: No indication of purpose
<button onClick={handleClose}>
<Icon name="close" />
</button>
// ✅ Good: Clear label
<button
onClick={handleClose}
aria-label="Close dialog"
>
<Icon name="close" />
</button>WCAG Compliance
Level AA Requirements
All components must meet WCAG 2.1 Level AA:
- Perceivable: Text alternatives, captions, color contrast
- Operable: Keyboard accessible, no seizures, enough time
- Understandable: Readable, predictable, input assistance
- Robust: Compatible with assistive technologies
Related Resources
- Accessibility as System Infrastructure — Deep dive into accessibility philosophy
- Props & API Standards — How props support accessibility
- WCAG 2.1 Quick Reference — Official WCAG guidelines