Focus Trap is a vanilla JavaScript library that traps focus within a DOM node, essential for building accessible modals, menus, and interactive UI components. It handles Tab and Shift+Tab navigation cycles within designated containers, blocks clicks outside the trap, supports Escape key deactivation, and automatically restores focus to the previously focused element when deactivated.
npm install focus-trapimport { createFocusTrap } from 'focus-trap';For CommonJS:
const { createFocusTrap } = require('focus-trap');For UMD (browser):
<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>
// Available as global: focusTrap.createFocusTrap
</script>import { createFocusTrap } from 'focus-trap';
// Create a focus trap on a modal element
const container = document.getElementById('modal');
const focusTrap = createFocusTrap('#modal', {
onActivate: () => container.classList.add('is-active'),
onDeactivate: () => container.classList.remove('is-active'),
});
// Activate the trap when modal opens
document.getElementById('open-modal').addEventListener('click', () => {
focusTrap.activate();
});
// Deactivate when modal closes
document.getElementById('close-modal').addEventListener('click', () => {
focusTrap.deactivate();
});Focus Trap is built around several key components:
createFocusTrap() function creates trap instances for specific containersCore functionality for creating and managing focus traps on DOM elements.
function createFocusTrap(
elements: HTMLElement | SVGElement | string | Array<HTMLElement | SVGElement | string>,
userOptions?: Options
): FocusTrap;Methods for activating, deactivating, pausing, and updating focus traps.
interface FocusTrap {
readonly active: boolean;
readonly paused: boolean;
activate(activateOptions?: ActivateOptions): FocusTrap;
deactivate(deactivateOptions?: DeactivateOptions): FocusTrap;
pause(pauseOptions?: PauseOptions): FocusTrap;
unpause(unpauseOptions?: UnpauseOptions): FocusTrap;
updateContainerElements(
containerElements: HTMLElement | SVGElement | string | Array<HTMLElement | SVGElement | string>
): FocusTrap;
}Extensive configuration system for customizing trap behavior, focus targets, and event handling.
interface Options {
// Lifecycle callbacks
onActivate?: () => void;
onPostActivate?: () => void;
onDeactivate?: () => void;
onPostDeactivate?: () => void;
// Focus management
initialFocus?: FocusTargetOrFalse | undefined | (() => void);
fallbackFocus?: FocusTarget;
returnFocusOnDeactivate?: boolean;
setReturnFocus?: FocusTargetValueOrFalse | ((previousActiveElement: HTMLElement | SVGElement) => FocusTargetValueOrFalse);
// User interaction
escapeDeactivates?: boolean | ((event: KeyboardEvent) => boolean);
clickOutsideDeactivates?: boolean | ((event: MouseEvent | TouchEvent) => boolean);
allowOutsideClick?: boolean | ((event: MouseEvent | TouchEvent) => boolean);
// Other options
preventScroll?: boolean;
delayInitialFocus?: boolean;
document?: Document;
tabbableOptions?: FocusTrapTabbableOptions;
trapStack?: Array<FocusTrap>;
isKeyForward?: (event: KeyboardEvent) => boolean;
isKeyBackward?: (event: KeyboardEvent) => boolean;
}Focus-trap relies on the tabbable library to identify focusable and tabbable elements within containers.
// Functions from tabbable used internally by focus-trap
import {
tabbable, // Gets tabbable elements in tab order
focusable, // Gets all focusable elements
isFocusable, // Checks if single element is focusable
isTabbable, // Checks if single element is tabbable
getTabIndex // Gets computed tab index value
} from 'tabbable';The tabbableOptions configuration passes through to these tabbable functions to control:
Focus-trap throws errors in specific scenarios that should be handled:
// Common error conditions
type FocusTrapError =
| 'NO_TABBABLE_ELEMENTS' // No focusable elements found and no fallbackFocus
| 'INVALID_SELECTOR' // CSS selector doesn't match any elements
| 'POSITIVE_TABINDEX' // Positive tabindex in multi-container setup
| 'CONTAINER_NOT_FOUND'; // Container element no longer in DOM
// Error handling pattern
try {
const trap = createFocusTrap('#container');
trap.activate();
} catch (error) {
if (error.message.includes('at least one tabbable element')) {
// Handle missing focusable elements
console.log('Add focusable elements or use fallbackFocus option');
}
}type FocusTargetValue = HTMLElement | SVGElement | string;
type FocusTargetValueOrFalse = FocusTargetValue | false;
type FocusTarget = FocusTargetValue | (() => FocusTargetValue);
type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse);
interface FocusTrapTabbableOptions {
/** How to check if an element is displayed (affects performance) */
displayCheck?: 'full' | 'legacy-full' | 'non-zero-area' | 'none';
/** Function to get shadow root for shadow DOM elements */
getShadowRoot?: (node: Element) => ShadowRoot | boolean;
/** Whether to include the container element itself as tabbable */
includeContainer?: boolean;
}
type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean;
type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean;
// Activation/Deactivation/Pause option types
interface ActivateOptions {
onActivate?: () => void;
onPostActivate?: () => void;
checkCanFocusTrap?: (containers: Array<HTMLElement | SVGElement>) => Promise<void>;
}
interface DeactivateOptions {
returnFocus?: boolean;
onDeactivate?: () => void;
onPostDeactivate?: () => void;
checkCanReturnFocus?: (trigger: HTMLElement | SVGElement) => Promise<void>;
}
interface PauseOptions {
onPause?: () => void;
onPostPause?: () => void;
}
interface UnpauseOptions {
onUnpause?: () => void;
onPostUnpause?: () => void;
}