trapFocus
Traps focus within a container element.
1/**
2 * Traps focus within a container element.
3 *
4 * @param container - The DOM element to trap focus within.
5 * @returns A cleanup function to remove the focus trap.
6 */
7export function trapFocus(container: HTMLElement): () => void {
8 const focusableSelectors = [
9 'a[href]',
10 'button:not([disabled])',
11 'textarea:not([disabled])',
12 'input:not([disabled]):not([type="hidden"])',
13 'select:not([disabled])',
14 '[tabindex]:not([tabindex="-1"])'
15 ];
16
17 const getFocusable = (): HTMLElement[] =>
18 Array.from(container.querySelectorAll<HTMLElement>(focusableSelectors.join(',')))
19 .filter(el => !el.hasAttribute('disabled') && el.tabIndex !== -1);
20
21 const handleKeyDown = (e: KeyboardEvent) => {
22 if (e.key !== 'Tab') return;
23
24 const focusable = getFocusable();
25 const first = focusable[0];
26 const last = focusable[focusable.length - 1];
27
28 if (e.shiftKey) {
29 if (document.activeElement === first) {
30 e.preventDefault();
31 last.focus();
32 }
33 } else {
34 if (document.activeElement === last) {
35 e.preventDefault();
36 first.focus();
37 }
38 }
39 };
40
41 document.addEventListener('keydown', handleKeyDown);
42
43 return () => {
44 document.removeEventListener('keydown', handleKeyDown);
45 };
46}
Improves Accessibility
Ensures keyboard users can’t tab out of modal dialogs, popups, or overlays, aligning with WCAG guidelines.
Lightweight and Dependency-Free
Implements focus trapping with pure DOM APIs — no external libraries required.
Context-Aware Filtering
Dynamically gathers only currently focusable elements, skipping hidden or disabled controls.
Clean Lifecycle Management
Provides a cleanup function to easily disable the trap and remove event listeners.
Keyboard Navigation Support
Fully supports forward and backward tabbing (
Tab
andShift+Tab
) for intuitive keyboard behavior.
Tests | Examples
1let container: HTMLElement;
2let cleanup: () => void;
3let button1: HTMLButtonElement;
4let button2: HTMLButtonElement;
5
6beforeEach(() => {
7 container = document.createElement('div');
8 button1 = document.createElement('button');
9 button2 = document.createElement('button');
10 button1.textContent = 'First';
11 button2.textContent = 'Last';
12 container.append(button1, button2);
13 document.body.appendChild(container);
14 cleanup = trapFocus(container);
15});
16
17afterEach(() => {
18 cleanup();
19 document.body.innerHTML = '';
20});
21
22const triggerTab = (shiftKey = false) => {
23 const event = new KeyboardEvent('keydown', {
24 key: 'Tab',
25 bubbles: true,
26 cancelable: true,
27 shiftKey,
28 });
29 document.dispatchEvent(event);
30};
31
32test('wraps focus forward from last to first element', () => {
33 button2.focus();
34 triggerTab(); // Tab forward
35 expect(document.activeElement).toBe(button1);
36});
37
38test('wraps focus backward from first to last element', () => {
39 button1.focus();
40 triggerTab(true); // Shift + Tab
41 expect(document.activeElement).toBe(button2);
42});
Common Use Cases
Modal Dialogs
Prevent users from tabbing outside a modal until it’s dismissed.
Off-Canvas Menus
Keep keyboard focus inside mobile menus or drawers.
Custom Popups or Tooltips
Trap focus in popover components or contextual overlays.
Accessibility Enhancements
Add focus management to assistive UI for users relying on screen readers or keyboard navigation.
Form Wizards or Stepped Interfaces
Restrict tab navigation within a step until the user proceeds.