javascript 51 lines · 8 steps

How to trap keyboard focus in a dialog

A focus trap keeps Tab navigation cycling inside a modal and restores focus when it closes.

Explained by highlit
1const FOCUSABLE = [
2 'a[href]',
3 'button:not([disabled])',
4 'input:not([disabled])',
5 'select:not([disabled])',
6 'textarea:not([disabled])',
7 '[tabindex]:not([tabindex="-1"])',
8].join(',');
9 
10export function trapFocus(dialog) {
11 const previouslyFocused = document.activeElement;
12 
13 const getFocusable = () =>
14 Array.from(dialog.querySelectorAll(FOCUSABLE)).filter(
15 (el) => el.offsetParent !== null || el === document.activeElement
16 );
17 
18 const handleKeydown = (event) => {
19 if (event.key !== 'Tab') return;
20 
21 const focusable = getFocusable();
22 if (focusable.length === 0) {
23 event.preventDefault();
24 return;
25 }
26 
27 const first = focusable[0];
28 const last = focusable[focusable.length - 1];
29 const active = document.activeElement;
30 
31 if (event.shiftKey && (active === first || !dialog.contains(active))) {
32 event.preventDefault();
33 last.focus();
34 } else if (!event.shiftKey && active === last) {
35 event.preventDefault();
36 first.focus();
37 }
38 };
39 
40 dialog.addEventListener('keydown', handleKeydown);
41 
42 const initial = dialog.querySelector('[autofocus]') || getFocusable()[0] || dialog;
43 initial.focus();
44 
45 return function release() {
46 dialog.removeEventListener('keydown', handleKeydown);
47 if (previouslyFocused instanceof HTMLElement) {
48 previouslyFocused.focus();
49 }
50 };
51}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A focus trap intercepts Tab and Shift+Tab at the boundaries to wrap focus back inside the container.
  2. 2Querying focusable elements live on each keypress keeps the trap correct as the DOM changes.
  3. 3Returning a cleanup function lets the caller undo listeners and restore prior focus in one call.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How to trap keyboard focus in a dialog — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code