This guide provides comprehensive instructions for implementing WCAG 2.1 AA compliant accessibility features across the learning platform.
// src/app/layout.tsx or your root component
import { AccessibilityProvider } from '@/app/components/accessibility/AccessibilityProvider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AccessibilityProvider
enableNavigator={true}
enableScreenReader={true}
enableContrastChecker={true}
enableTester={true}
>
{children}
</AccessibilityProvider>
</body>
</html>
);
}<div className="app">
{/* Skip Links */}
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>
{/* Header */}
<header role="banner">
<h1>Your App Title</h1>
</header>
{/* Navigation */}
<nav id="main-navigation" role="navigation" aria-label="Main navigation">
{/* Navigation items */}
</nav>
{/* Main Content */}
<main id="main-content" role="main">
{/* Your content */}
</main>
{/* Footer */}
<footer id="footer" role="contentinfo">
{/* Footer content */}
</footer>
</div>Visit /accessibility-demo to see examples and test the features.
import { AccessibleError } from '@/app/components/accessibility/ScreenReaderOptimizer';
<form onSubmit={handleSubmit} aria-label="Contact form">
<label htmlFor="email">
Email <span className="text-red-600" aria-label="required">*</span>
</label>
<input
type="email"
id="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<div id="email-error" role="alert">
<AccessibleError message={errors.email} />
</div>
)}
</form>import { useFocusTrap } from '@/hooks/useAccessibility';
function Modal({ isOpen, onClose, title, children }) {
const containerRef = useFocusTrap(isOpen);
return (
<div
ref={containerRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<h2 id="modal-title">{title}</h2>
{children}
</div>
);
}import { AccessibleLoading } from '@/app/components/accessibility/ScreenReaderOptimizer';
<AccessibleLoading message="Loading courses" isLoading={isLoading} />import { AccessibleProgress } from '@/app/components/accessibility/ScreenReaderOptimizer';
<AccessibleProgress
value={75}
max={100}
label="Course completion"
showPercentage={true}
/>import { useScreenReaderAnnouncement } from '@/hooks/useAccessibility';
function MyComponent() {
const announce = useScreenReaderAnnouncement();
const handleAction = () => {
// Perform action
announce('Action completed successfully', 'polite');
};
}- 1.1.1 Non-text Content: All images have alt text
- 1.3.1 Info and Relationships: Semantic HTML and ARIA labels
- 1.3.2 Meaningful Sequence: Logical reading order
- 1.4.3 Contrast (Minimum): 4.5:1 for normal text, 3:1 for large text
- 1.4.4 Resize Text: Text can be resized to 200%
- 1.4.11 Non-text Contrast: UI components have 3:1 contrast
- 2.1.1 Keyboard: All functionality available via keyboard
- 2.1.2 No Keyboard Trap: Focus can move away from all components
- 2.4.1 Bypass Blocks: Skip links provided
- 2.4.2 Page Titled: Pages have descriptive titles
- 2.4.3 Focus Order: Logical tab order
- 2.4.4 Link Purpose: Link text describes destination
- 2.4.7 Focus Visible: Visible focus indicators
- 3.1.1 Language of Page: HTML lang attribute set
- 3.2.1 On Focus: No unexpected context changes on focus
- 3.2.2 On Input: No unexpected context changes on input
- 3.3.1 Error Identification: Errors clearly identified
- 3.3.2 Labels or Instructions: Form inputs have labels
- 3.3.3 Error Suggestion: Error correction suggestions provided
- 4.1.1 Parsing: Valid HTML
- 4.1.2 Name, Role, Value: ARIA attributes for custom components
- 4.1.3 Status Messages: Live regions for status updates
-
Run Accessibility Tester
- Click the green button (bottom-right)
- Click "Run Accessibility Check"
- Review and fix all critical and serious issues
-
Check Color Contrast
- Click the purple button (middle-right)
- Click "Check Page Contrast"
- Fix any failing contrast ratios
-
Run Unit Tests
npm test
-
Keyboard Navigation
- Unplug your mouse
- Navigate entire site using only keyboard
- Verify all interactive elements are reachable
- Check focus indicators are visible
-
Screen Reader Testing
- Windows: NVDA (free) or JAWS
- Mac: VoiceOver (built-in)
- Mobile: TalkBack (Android) or VoiceOver (iOS)
Test checklist:
- All content is announced
- Form labels are read correctly
- Buttons have clear names
- Headings provide structure
- Links describe their destination
- Status messages are announced
-
Zoom Testing
- Zoom to 200% (Ctrl/Cmd + +)
- Verify all content is readable
- Check for horizontal scrolling
- Ensure no content is cut off
-
Color Blindness Testing
- Use browser extensions (e.g., "Colorblind - Dalton")
- Test with different color blindness types
- Verify information isn't conveyed by color alone
Problem: Images without alt attributes Solution:
// Decorative image
<img src="decoration.png" alt="" />
// Informative image
<img src="chart.png" alt="Sales increased 25% in Q4" />Problem: Inputs without associated labels Solution:
// Option 1: Explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Option 2: Implicit label
<label>
Email
<input type="email" />
</label>
// Option 3: ARIA label
<input type="email" aria-label="Email address" />Problem: Text doesn't meet 4.5:1 contrast ratio Solution:
/* Bad: 2.5:1 contrast */
.text { color: #999; background: #fff; }
/* Good: 4.6:1 contrast */
.text { color: #767676; background: #fff; }
/* Better: 7:1 contrast */
.text { color: #595959; background: #fff; }Problem: Focus gets stuck in a component Solution:
import { useFocusTrap } from '@/hooks/useAccessibility';
// Use focus trap hook for modals
const containerRef = useFocusTrap(isOpen);
// Ensure Escape key closes modal
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);Problem: Can't see which element has focus Solution:
/* Global focus styles already added in globals.css */
*:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Custom focus for specific elements */
.button:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
}// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>// Bad
<button><Icon /></button>
// Good
<button aria-label="Close dialog">
<Icon aria-hidden="true" />
</button>// Bad
<h1>Page Title</h1>
<h3>Section</h3> {/* Skipped h2 */}
// Good
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>// Bad - unnecessary ARIA
<button role="button">Click</button>
// Good - ARIA only when needed
<div role="button" tabIndex={0} onClick={handleClick}>
Custom Button
</div>import { useScreenReaderAnnouncement } from '@/hooks/useAccessibility';
const announce = useScreenReaderAnnouncement();
// Announce important changes
useEffect(() => {
if (dataLoaded) {
announce('Data loaded successfully', 'polite');
}
}, [dataLoaded, announce]);- WCAG 2.1 Guidelines
- ARIA Authoring Practices
- WebAIM Articles
- Deque University
- A11y Project Checklist
For questions or issues:
- Check the README in
src/app/components/accessibility/ - Review examples in
src/app/components/accessibility/examples/ - Test on the demo page at
/accessibility-demo
While these tools provide comprehensive automated accessibility checking, they cannot catch all accessibility issues. Manual testing with assistive technologies and real users with disabilities is essential for true accessibility compliance.
This implementation provides tools to help achieve WCAG 2.1 AA compliance but does not guarantee it. Ongoing testing, user feedback, and remediation are required to maintain accessibility standards.