Best Practices: Accessibility
When to Use
Every AJAX implementation must meet WCAG 2.1 Level AA standards.
Accessibility Requirements
Accessibility Requirements:
- Screen Reader Announcements
- Announce all content updates using AnnounceCommand or MessageCommand
- Use 'polite' priority for non-critical updates
- Use 'assertive' only for errors requiring immediate attention
-
Provide meaningful context ("Search results updated with 5 items" not "Updated")
-
Keyboard Navigation
- Add
'keypress' => TRUEto all AJAX buttons - Manage focus after updates with FocusFirstCommand
- Ensure all triggers are keyboard-accessible (no click-only elements)
-
Test with Tab, Enter, Space, Esc keys
-
Focus Management
- Return focus to logical element after update
- Don't move focus unexpectedly (confuses users)
- Use FocusFirstCommand for new content regions
-
Close dialogs with Esc key (built-in to dialog system)
-
Loading Indicators
- Provide progress messages read by screen readers
- Show visual loading states (spinners, progress bars)
- Disable triggering element during processing (prevent double-submit)
-
Clear loading state after completion
-
ARIA Attributes
- Use
aria-live="polite"for dynamic regions - Use
aria-atomic="true"to read entire updated region - Add
aria-busy="true"during loading - Mark expanded/collapsed states with
aria-expanded
Accessibility Testing Checklist
Accessibility Testing Checklist:
- [ ] Unplug mouse, navigate entire workflow with keyboard only
- [ ] Test with NVDA (Windows), JAWS (Windows), or VoiceOver (Mac)
- [ ] Verify all AJAX triggers are keyboard-accessible
- [ ] Confirm screen reader announces all content changes
- [ ] Check focus doesn't get lost after updates
- [ ] Verify loading indicators are announced
- [ ] Test with browser zoom at 200%
- [ ] Run automated tests with axe DevTools or WAVE
Pattern
// Screen reader announcements
$response->addCommand(new AnnounceCommand('Results updated with 5 items', 'polite'));
// Keyboard trigger
$form['trigger']['#ajax']['keypress'] = TRUE;
// Focus management
$response->addCommand(new FocusFirstCommand('#new-content-region'));
// ARIA attributes on dynamic regions
$form['results'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'results',
'aria-live' => 'polite',
'aria-atomic' => 'true',
'aria-busy' => 'false', // Set to 'true' during loading via InvokeCommand
],
];
// Loading indicator with accessible message
$form['trigger']['#ajax']['progress'] = [
'type' => 'throbber',
'message' => t('Loading, please wait...'),
];