What is focus management?
Focus management is the practice of programmatically controlling where keyboard focus goes in response to user actions. It's critical for:
- Single-page applications (SPAs)
- Modal dialogs and overlays
- Dynamic content updates
- Error handling in forms
- Tab interfaces and accordions
Poor focus management can leave keyboard users lost on a page, unable to continue their task.
Focus indicators (WCAG 2.4.7)
Focus indicators show which element currently has keyboard focus.
Requirements
- Must be visible for all focusable elements
- Should have sufficient contrast against all backgrounds
- Should not be removed without replacement
Recommended CSS approach
/* Use :focus-visible for keyboard-only focus */
:focus-visible {
outline: 3px solid #0C234B;
outline-offset: 2px;
}
/* Fallback for browsers without :focus-visible */
:focus:not(:focus-visible) {
outline: none;
}
/* High contrast for dark backgrounds */
.dark-bg :focus-visible {
outline-color: #FFD700;
}
Never do this
/* โ Removes all focus indicators */
*:focus {
outline: none;
}
/* โ Hides focus on specific elements */
a:focus, button:focus {
outline: 0;
}
Focus order (WCAG 2.4.3)
Focus order should match the visual reading order and preserve meaning.
Best practices
- Use semantic HTML in logical source order
- Avoid positive
tabindexvalues (1, 2, 3...) - Test by tabbing through without mouse
- Ensure CSS layout doesn't create visual/focus mismatch
CSS layout considerations
/* Flexbox can reorder visually but not focus order */
.flex-container {
display: flex;
flex-direction: row-reverse; /* Visual order changed */
}
/* Focus still follows DOM order - may confuse users */
/* If you must reorder, consider matching tabindex or DOM order */
Focus not obscured (WCAG 2.4.11)
New in WCAG 2.2: When an element receives focus, it must not be entirely hidden by other content.
Common problems
- Sticky headers covering focused elements
- Cookie consent banners
- Chat widgets
- Fixed navigation
Solutions
/* Use scroll-margin to offset for sticky headers */
[id] {
scroll-margin-top: 80px; /* Height of sticky header */
}
/* Or scroll-padding on the container */
html {
scroll-padding-top: 80px;
}
/* Ensure modals don't cover focused content behind them */
.modal-open {
/* Trap focus inside modal instead */
}
Modal dialog focus management
Modal dialogs require careful focus management to be accessible.
Requirements
- On open: Move focus into the modal (first focusable element or modal itself)
- Focus trap: Keep focus inside modal while open
- On close: Return focus to the element that opened the modal
- Escape key: Close modal and return focus
Implementation pattern
// Store trigger element
let triggerElement;
function openModal(modal) {
triggerElement = document.activeElement;
modal.setAttribute('aria-hidden', 'false');
modal.style.display = 'block';
// Focus first focusable element or modal
const firstFocusable = modal.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
firstFocusable.focus();
} else {
modal.setAttribute('tabindex', '-1');
modal.focus();
}
}
function closeModal(modal) {
modal.setAttribute('aria-hidden', 'true');
modal.style.display = 'none';
// Return focus to trigger
if (triggerElement) {
triggerElement.focus();
}
}
Focus trap implementation
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', function(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();
}
}
if (e.key === 'Escape') {
closeModal(modal);
}
});
}
Single-page application focus
SPAs don't trigger page loads, so focus management must be handled manually.
Route change patterns
- Focus heading: Move focus to the new page's H1
- Focus container: Focus main content area with
tabindex="-1" - Announce change: Use ARIA live region to announce new page
Example: React route change
// Focus main content on route change
useEffect(() => {
const mainContent = document.getElementById('maincontent');
if (mainContent) {
mainContent.tabIndex = -1;
mainContent.focus();
// Remove tabindex after focus to prevent re-focus issues
mainContent.addEventListener('blur', () => {
mainContent.removeAttribute('tabindex');
}, { once: true });
}
}, [location.pathname]);
Dynamic content focus
Form error handling
When form validation fails:
- Move focus to error summary or first error
- Announce errors to screen readers
- Link errors to specific fields
function handleFormErrors(errors) {
// Create or update error summary
const summary = document.getElementById('error-summary');
summary.innerHTML = `<h2>${errors.length} errors found</h2>...`;
// Focus error summary
summary.tabIndex = -1;
summary.focus();
// Announce to screen readers
summary.setAttribute('role', 'alert');
}
Infinite scroll / Load more
When new content loads:
- Don't automatically move focus
- Announce new content with live region
- Keep focus on "Load more" button
Delete actions
When an item is deleted:
- Move focus to next item in list
- Or move to previous item if last was deleted
- Or move to container if list is empty
Testing focus management
Manual testing
- Tab through entire page without mouse
- Open/close modals and verify focus returns
- Trigger route changes and check focus
- Submit forms with errors
- Verify focus is never lost off-screen
Automated tools
- Accessibility Insights: Tab stops visualization
- axe DevTools: Focus order issues
- Browser DevTools:
document.activeElementin console
Debug tip
// Log focus changes in console
document.addEventListener('focusin', (e) => {
console.log('Focus moved to:', e.target);
});
Focus management checklist
- โ All focusable elements have visible focus indicator
- โ Focus order matches reading order
- โ Focus not obscured by sticky/fixed elements
- โ Modals trap focus and return focus on close
- โ Route changes move focus appropriately
- โ Form errors focus error summary or first error
- โ Dynamic content doesn't cause focus loss
- โ Escape key closes modals/menus
- โ No positive tabindex values used