Focus Management
Focus management strategies for single-page applications, modals, route changes, and dynamic content
You are an expert in focus management for building accessible single-page applications. ## Key Points - **Route/view changes**: After client-side navigation, focus should move to the new content. - **Modal dialogs**: Focus must be trapped inside the modal and restored when it closes. - **Dynamic content insertion**: When content appears (toast notifications, inline expansions, search results), focus or an announcement must inform the user. - **Element removal**: When the focused element is removed from the DOM, focus must be moved to a logical successor. - **Multi-step flows**: Wizards and steppers should move focus to the new step heading or first interactive element. - After every client-side navigation, confirm that focus moves to a meaningful element (heading or main content area) and the screen reader announces the new context. - Open a modal, Tab through every focusable element, and confirm focus wraps within the modal without escaping. - Close the modal and verify focus returns to the element that triggered it. - Delete items from a list and verify focus moves to a logical successor rather than resetting to the top of the page. - Test with NVDA or VoiceOver to confirm that announcements fire at the right time. - Use the native `<dialog>` element with `showModal()` when possible; it provides built-in focus trapping and Escape key handling. - Set `tabindex="-1"` on non-interactive targets (like headings) that receive programmatic focus so they do not appear in the regular Tab order.
skilldb get accessibility-skills/Focus ManagementFull skill: 204 linesFocus Management — Web Accessibility
You are an expert in focus management for building accessible single-page applications.
Core Philosophy
Overview
In traditional multi-page websites, the browser resets focus to the top of the page on navigation. Single-page applications (SPAs) update content without a full page reload, which means focus can be left on a stale element, a removed element, or nowhere meaningful. Proper focus management ensures keyboard and screen reader users always know where they are and what changed.
Core Concepts
When focus management is needed
- Route/view changes: After client-side navigation, focus should move to the new content.
- Modal dialogs: Focus must be trapped inside the modal and restored when it closes.
- Dynamic content insertion: When content appears (toast notifications, inline expansions, search results), focus or an announcement must inform the user.
- Element removal: When the focused element is removed from the DOM, focus must be moved to a logical successor.
- Multi-step flows: Wizards and steppers should move focus to the new step heading or first interactive element.
tabindex values
| Value | Meaning |
|---|---|
tabindex="0" | Element is focusable in natural DOM order |
tabindex="-1" | Element is focusable programmatically but not via Tab |
tabindex="1+" | Forces focus order position — avoid this |
Implementation Patterns
Focus on route change (React)
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
function PageWrapper({ title, children }) {
const headingRef = useRef(null);
const location = useLocation();
useEffect(() => {
document.title = title;
// Move focus to the page heading after route change
headingRef.current?.focus();
}, [location.pathname, title]);
return (
<main>
<h1 ref={headingRef} tabIndex={-1}>
{title}
</h1>
{children}
</main>
);
}
Focus trap for modals
function trapFocus(modalElement) {
const focusableSelectors = [
'a[href]', 'button:not([disabled])', 'input:not([disabled])',
'select:not([disabled])', 'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
];
const focusableElements = modalElement.querySelectorAll(
focusableSelectors.join(', ')
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
function handleKeydown(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
modalElement.addEventListener('keydown', handleKeydown);
// Focus the first element
firstFocusable?.focus();
// Return cleanup function
return () => modalElement.removeEventListener('keydown', handleKeydown);
}
Using the native dialog element
<dialog id="confirm-dialog" aria-labelledby="dialog-heading">
<h2 id="dialog-heading">Confirm action</h2>
<p>Are you sure you want to proceed?</p>
<button id="cancel-btn">Cancel</button>
<button id="confirm-btn">Confirm</button>
</dialog>
const dialog = document.getElementById('confirm-dialog');
const openButton = document.getElementById('open-dialog');
openButton.addEventListener('click', () => {
dialog.showModal(); // Native focus trap and Escape handling
});
dialog.addEventListener('close', () => {
openButton.focus(); // Restore focus to the trigger
});
Focus restoration after element removal
function deleteItem(itemElement, listElement) {
const items = Array.from(listElement.children);
const index = items.indexOf(itemElement);
itemElement.remove();
// Focus the next item, or the previous if last was deleted, or the list itself
const remaining = Array.from(listElement.children);
if (remaining[index]) {
remaining[index].focus();
} else if (remaining[index - 1]) {
remaining[index - 1].focus();
} else {
listElement.focus(); // Empty list — focus the container
}
}
Announcing route changes with a live region
function RouteAnnouncer() {
const [announcement, setAnnouncement] = useState('');
const location = useLocation();
useEffect(() => {
setAnnouncement(`Navigated to ${document.title}`);
}, [location.pathname]);
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
);
}
Testing & Validation
- After every client-side navigation, confirm that focus moves to a meaningful element (heading or main content area) and the screen reader announces the new context.
- Open a modal, Tab through every focusable element, and confirm focus wraps within the modal without escaping.
- Close the modal and verify focus returns to the element that triggered it.
- Delete items from a list and verify focus moves to a logical successor rather than resetting to the top of the page.
- Test with NVDA or VoiceOver to confirm that announcements fire at the right time.
Best Practices
- Use the native
<dialog>element withshowModal()when possible; it provides built-in focus trapping and Escape key handling. - Set
tabindex="-1"on non-interactive targets (like headings) that receive programmatic focus so they do not appear in the regular Tab order. - Always restore focus to the triggering element when closing overlays, popovers, or dialogs.
Common Pitfalls
- Failing to manage focus on route changes, leaving the user stranded on a button or link that no longer corresponds to the visible content.
- Calling
.focus()on an element before it is rendered in the DOM; userequestAnimationFrameoruseEffectto defer the call.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add accessibility-skills
Related Skills
Accessible Forms
Accessible form design patterns including labels, validation, error handling, and multi-step forms
Aria Patterns
ARIA roles, states, and properties for building accessible custom widgets and UI components
Axe Testing
Automated accessibility testing with axe-core, including CI integration, custom rules, and result analysis
Color Contrast
Color contrast ratios, visual accessibility, and inclusive design for users with low vision or color blindness
Keyboard Navigation
Keyboard navigation patterns, focus order, and shortcut design for fully keyboard-accessible interfaces
Screen Reader Compat
Building web content that works correctly with screen readers like NVDA, JAWS, and VoiceOver