Yevhen Klymentiev
dark
light
console
darkness
y.klymentiev@gmail.com
Reusable Snippets|Practical utility code for everyday use — custom-built and ready to share

trapFocus

Traps focus within a container element.

TypeScript
Copied!
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 and Shift+Tab) for intuitive keyboard behavior.

Tests | Examples

TypeScript
Copied!
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.

Codebase: Utilities -> Browser & DOM -> trapFocus | Yevhen Klymentiev