# Focus Trap

> Canonical HTML version: https://rocketvalidator.com/glossary/focus-trap
> Attribution: Rocket Validator (https://rocketvalidator.com)
> License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

A focus trap is a technique that constrains keyboard focus within a specific region of a page—such as a modal dialog—so that pressing Tab or Shift+Tab cycles only through the interactive elements inside that region until the user explicitly dismisses it.

When a user opens an overlay component like a modal dialog, a dropdown menu, or a confirmation prompt, keyboard focus should remain inside that component until the user intentionally closes it. Without this constraint, pressing Tab can move focus behind the overlay and onto elements the user cannot see, creating a confusing and sometimes impossible-to-navigate experience. A focus trap solves this by intercepting keyboard navigation at the boundaries of the component and looping focus back to the opposite end.

Focus traps are not the same as "keyboard traps," which is a negative accessibility pattern where focus gets stuck and the user has no way to escape. A well-implemented focus trap always provides at least one clear exit mechanism—typically the Escape key or an explicit close button—that returns focus to the element that triggered the component in the first place.

## Why focus traps matter

Focus management is a cornerstone of keyboard accessibility. Sighted mouse users can simply click the close button on a modal, but keyboard-only users, screen reader users, and people who rely on switch devices depend entirely on the logical flow of focus. WCAG 2.1 Success Criterion 2.1.2 (No Keyboard Trap) explicitly requires that users can move focus away from any component using standard keystrokes. A focus trap that follows best practices satisfies this criterion while also meeting the spirit of SC 2.4.3 (Focus Order) by keeping the navigation sequence meaningful.

Without a focus trap:

- A keyboard user pressing Tab in a modal may land on a page element hidden behind a dimmed backdrop, with no visual or programmatic indication of where they are.
- A screen reader may announce content the user did not intend to interact with, breaking the mental model of the dialog.
- The user may be unable to return to the modal at all, effectively losing access to the dialog's controls.

Focus traps affect every user who does not rely exclusively on a pointing device—a group that includes power users, people with motor impairments, people with low vision, and anyone navigating with assistive technology.

## How focus traps work

### Identifying focusable elements

When the trapped region opens, the script collects all focusable elements inside it. Focusable elements typically include `<a>` with an `href`, `<button>`, `<input>`, `<select>`, `<textarea>`, and any element with a non-negative `tabindex`. Elements that are hidden (`display: none`, `visibility: hidden`) or disabled are excluded.

### Intercepting Tab and Shift+Tab

A `keydown` event listener watches for the Tab key. When the user presses Tab on the **last** focusable element, focus is programmatically moved to the **first** focusable element. When the user presses Shift+Tab on the **first** focusable element, focus moves to the **last**. This creates a seamless loop.

### Providing an exit

The Escape key should close the component and return focus to the triggering element. A visible close button should do the same. Both paths release the trap and restore the page's normal tab order.

### Setting initial focus

When the modal opens, focus should move to the first interactive element inside it—or to the dialog container itself if it has `tabindex="-1"` and a descriptive label. This signals to assistive technology that context has changed.

## Code examples

### Bad example — no focus trap

In this example the modal opens but focus is never constrained. A keyboard user pressing Tab will escape the dialog and land on background content.

```html
<div id="modal" role="dialog" aria-labelledby="modal-title" aria-modal="true">
  <h2 id="modal-title">Confirm deletion</h2>
  <p>Are you sure you want to delete this item?</p>
  <button id="confirm-btn">Delete</button>
  <button id="cancel-btn">Cancel</button>
</div>

<script>
  // Modal is shown, but no focus management happens.
  document.getElementById("modal").style.display = "block";
</script>
```

### Good example — focus trap with exit via Escape

```html
<div
  id="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  tabindex="-1"
>
  <h2 id="modal-title">Confirm deletion</h2>
  <p>Are you sure you want to delete this item?</p>
  <button id="confirm-btn">Delete</button>
  <button id="cancel-btn">Cancel</button>
</div>

<script>
  const trigger = document.getElementById("open-modal-btn");
  const modal = document.getElementById("modal");

  function openModal() {
    modal.style.display = "block";
    modal.focus();
    document.addEventListener("keydown", trapFocus);
  }

  function closeModal() {
    modal.style.display = "none";
    document.removeEventListener("keydown", trapFocus);
    trigger.focus(); // Return focus to the element that opened the modal
  }

  function getFocusableElements() {
    return modal.querySelectorAll(
      'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
    );
  }

  function trapFocus(event) {
    if (event.key === "Escape") {
      closeModal();
      return;
    }

    if (event.key !== "Tab") return;

    const focusable = getFocusableElements();
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (event.shiftKey) {
      if (document.activeElement === first) {
        event.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }
  }

  document.getElementById("cancel-btn").addEventListener("click", closeModal);
  trigger.addEventListener("click", openModal);
</script>
```

Key points in the good example:

- The `role="dialog"` and `aria-modal="true"` attributes tell assistive technology this is a modal context.
- `tabindex="-1"` on the dialog container allows it to receive programmatic focus without appearing in the natural tab order.
- The `trapFocus` function loops focus between the first and last interactive elements.
- Pressing Escape or clicking Cancel closes the modal **and** returns focus to the trigger button.
- The event listener is added when the modal opens and removed when it closes, keeping the page behavior clean.

By combining proper ARIA semantics, deliberate focus movement, and a reliable exit mechanism, a focus trap ensures that modal interactions are usable and understandable for everyone—regardless of how they navigate.
