Accessible Focus Trapping
focus-trap is a vanilla JavaScript library designed to trap keyboard focus within a specified DOM node, essential for building accessible UI components like modals, dialogs, and sidebars. It ensures that users navigating with keyboard (Tab, Shift+Tab) or screen readers cannot escape the designated area, enhancing accessibility. The current stable version is 8.0.1, with releases occurring as needed for bug fixes (patch), new features (minor), and breaking changes (major). Key differentiators include its lightweight, framework-agnostic nature, robust handling of nested traps (pausing/unpausing), and its reliance on the well-maintained `tabbable` library for determining focusable elements. It handles initial focus, tabbing within the trap, blocking clicks outside, and restoring focus on deactivation. It explicitly supports modern desktop browsers and offers UMD builds for environments without bundlers.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'activate')
cause `createFocusTrap` was called with a non-existent or null DOM element, resulting in `undefined` being returned. Subsequently, `undefined.activate()` throws an error.fixEnsure the DOM element passed to `createFocusTrap` exists and is a valid `HTMLElement`. Check for `null` or `undefined` returns from `document.getElementById` or `document.querySelector`. -
Focus trap is not working as expected (e.g., focus escapes the modal, tabbing order is incorrect, clicks outside are not blocked).
cause This is often due to the Safari 'Press Tab to highlight each item on a webpage' setting being disabled, or incorrectly configured `initialFocus` options, or elements within the trap not being correctly identified as tabbable by `tabbable`.fixFor Safari, ensure the specific setting is enabled. Verify that your elements are natively tabbable or correctly marked with `tabindex`. Review the `tabbable` library documentation for details on what elements are considered tabbable. Also, check for CSS properties like `display: none` or `visibility: hidden` on the trap's container or its tabbable children. -
ReferenceError: FocusTrap is not defined
cause Occurs when using the UMD build (e.g., from unpkg) but trying to access `FocusTrap` globally without it being loaded, or trying to destructure `createFocusTrap` when only `window.FocusTrap` is available.fixEnsure the UMD script for `focus-trap` is properly loaded. When loaded via UMD, the exports are typically available under `window.FocusTrap`, so you would access `window.FocusTrap.createFocusTrap`.
Warnings
- breaking The `onPostActivate()` callback now correctly executes *after* the initial focus node has received focus and the trap is fully activated. Previously, it would fire prematurely. This aligns the behavior with the intended purpose of `onPostActivate()`.
- gotcha In Safari, the default browser settings prevent tabbing through all elements on a webpage. This significantly impacts how `focus-trap` determines and cycles through tabbable elements, potentially causing traps to not work as expected.
- deprecated Support for Internet Explorer (all versions) has been officially dropped due to Microsoft's end-of-life for IE. The library no longer guarantees functionality or provides fixes for IE-specific issues.
- gotcha When using the UMD build (`dist/focus-trap.umd.js`) directly in the browser without a module bundler, the `tabbable` dependency is *not* bundled with `focus-trap`. It must be included separately and loaded *before* `focus-trap`.
Install
-
npm install focus-trap -
yarn add focus-trap -
pnpm add focus-trap
Imports
- createFocusTrap
import createFocusTrap from 'focus-trap';
import { createFocusTrap } from 'focus-trap'; - FocusTrap
import type { FocusTrap } from 'focus-trap'; - createFocusTrap (CommonJS)
const createFocusTrap = require('focus-trap');const { createFocusTrap } = require('focus-trap'); - createFocusTrap (UMD)
<!-- Include tabbable first --> <script src="https://unpkg.com/tabbable/dist/index.umd.js"></script> <script src="https://unpkg.com/focus-trap/dist/focus-trap.umd.js"></script> <script> const { createFocusTrap } = window.FocusTrap; </script>
Quickstart
import { createFocusTrap } from 'focus-trap';
const modalElement = document.getElementById('my-modal');
const openButton = document.getElementById('open-modal-button');
const closeButton = document.getElementById('close-modal-button');
let focusTrapInstance: ReturnType<typeof createFocusTrap> | null = null;
function openModal() {
if (modalElement) {
modalElement.style.display = 'block';
focusTrapInstance = createFocusTrap(modalElement, {
onDeactivate: () => {
if (modalElement) modalElement.style.display = 'none';
openButton?.focus(); // Return focus to the element that opened the modal
},
initialFocus: closeButton || undefined, // Optionally focus the close button first
});
focusTrapInstance.activate();
}
}
function closeModal() {
if (focusTrapInstance) {
focusTrapInstance.deactivate();
focusTrapInstance = null;
}
}
openButton?.addEventListener('click', openModal);
closeButton?.addEventListener('click', closeModal);
// Example: simulate opening a modal after a delay
setTimeout(openModal, 1000);