⚡IntermediateAdvanced Accessibility
Focus Management
Control keyboard focus for SPAs, modals, and dynamic content with tabindex and focus().
Overview
Focus management is the practice of programmatically controlling which element receives keyboard focus in response to user interactions. It is critical in single-page applications, modals, dialogs, tabs, and any pattern where content changes without a full page reload.
Without proper focus management, keyboard users lose their position when:
- A modal opens (focus stays behind the overlay)
- A route changes (focus stays on the nav link)
- A panel closes (focus disappears)
- New content loads (focus stays outside the new content)
Key techniques: element.focus(), tabindex values, focus trapping in modals, and restoring focus when dialogs close.
Key Points
- •element.focus() programmatically moves keyboard focus to any focusable element
- •tabindex="0" makes non-interactive elements focusable; tabindex="-1" allows programmatic focus only
- •When a modal opens: move focus to the first interactive element or the dialog itself
- •When a modal closes: return focus to the element that triggered it
- •Focus trapping: in modals, Tab/Shift+Tab should cycle within the dialog only
- •In SPAs after route change: move focus to the new page's <h1> or a skip-to-content area
Examples
Opening and closing a modal with focus management
<button id="open-btn">Open Settings</button>
<dialog id="settings-modal" aria-labelledby="modal-title">
<h2 id="modal-title">Settings</h2>
<label for="theme">Theme</label>
<select id="theme" name="theme">
<option>Light</option>
<option>Dark</option>
</select>
<button id="close-btn">Close</button>
</dialog>
<script>
const openBtn = document.getElementById('open-btn');
const modal = document.getElementById('settings-modal');
const closeBtn = document.getElementById('close-btn');
openBtn.addEventListener('click', () => {
modal.showModal();
// Move focus to first interactive element in modal
document.getElementById('theme').focus();
});
closeBtn.addEventListener('click', () => {
modal.close();
// Return focus to the button that opened the modal
openBtn.focus();
});
</script>Focus trapping in a custom modal
function trapFocus(element) {
const focusable = element.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
element.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift+Tab on first element → wrap to last
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
// Tab on last element → wrap to first
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}SPA route change focus management
// After route change, move focus to page heading
router.afterEach(() => {
Vue.nextTick(() => {
const h1 = document.querySelector('main h1');
if (h1) {
h1.setAttribute('tabindex', '-1');
h1.focus();
}
});
});React Router, Next.js, and similar frameworks often need custom focus management hooks.
Common Mistakes
- Not returning focus when a modal closes keyboard users are left stranded
- Moving focus to a non-interactive element without tabindex="-1" focus() fails silently
- Using positive tabindex values (tabindex="1") disrupts natural tab order
Note
The native <dialog> element (showModal()) handles focus trapping and Escape key closing automatically. Use it instead of custom div-based modals whenever possible.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4