Extensive configuration system for customizing focus trap behavior, focus targets, event handling, and user interaction patterns. Configuration options control every aspect of how the trap manages focus and responds to user input.
Functions called at different points in the trap lifecycle for custom behavior integration.
interface LifecycleCallbacks {
/** Called before sending focus to target element on activation */
onActivate?: () => void;
/** Called after focus has been sent to target element on activation */
onPostActivate?: () => void;
/** Called before returning focus on deactivation */
onDeactivate?: () => void;
/** Called after trap is deactivated and focus returned */
onPostDeactivate?: () => void;
/** Called immediately when trap is paused */
onPause?: () => void;
/** Called after trap is completely paused */
onPostPause?: () => void;
/** Called immediately when trap is unpaused */
onUnpause?: () => void;
/** Called after trap is completely unpaused */
onPostUnpause?: () => void;
}Usage Examples:
const trap = createFocusTrap('#modal', {
onActivate: () => {
document.body.classList.add('modal-open');
console.log('Trap activating');
},
onPostActivate: () => {
console.log('Trap is now active');
},
onDeactivate: () => {
console.log('Trap deactivating');
},
onPostDeactivate: () => {
document.body.classList.remove('modal-open');
console.log('Trap deactivated');
}
});Options controlling where focus goes when the trap activates and deactivates.
interface FocusTargetOptions {
/** Element to focus when trap activates */
initialFocus?: FocusTargetOrFalse | undefined | (() => void);
/** Fallback element if no tabbable elements found */
fallbackFocus?: FocusTarget;
/** Whether to return focus to previous element on deactivation */
returnFocusOnDeactivate?: boolean;
/** Custom element to return focus to on deactivation */
setReturnFocus?: FocusTargetValueOrFalse | ((previousActiveElement: HTMLElement | SVGElement) => FocusTargetValueOrFalse);
}
type FocusTarget = FocusTargetValue | (() => FocusTargetValue);
type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse);
type FocusTargetValue = HTMLElement | SVGElement | string;
type FocusTargetValueOrFalse = FocusTargetValue | false;Usage Examples:
// Set initial focus to specific element
const trap1 = createFocusTrap('#dialog', {
initialFocus: '#dialog-title', // Focus title on activation
});
// Disable initial focus
const trap2 = createFocusTrap('#menu', {
initialFocus: false, // No initial focus
});
// Dynamic initial focus
const trap3 = createFocusTrap('#form', {
initialFocus: () => {
const firstError = document.querySelector('.error');
return firstError || '#first-input';
}
});
// Fallback for containers without tabbable elements
const trap4 = createFocusTrap('#empty-container', {
fallbackFocus: '#empty-container', // Container needs tabindex="-1"
});
// Custom return focus
const trap5 = createFocusTrap('#popup', {
setReturnFocus: (previousElement) => {
// Return focus to specific button instead of previous element
return document.querySelector('#main-button');
}
});
// Disable return focus
const trap6 = createFocusTrap('#notification', {
returnFocusOnDeactivate: false
});Configuration for how the trap responds to keyboard and mouse events.
interface UserInteractionOptions {
/** Whether Escape key deactivates the trap */
escapeDeactivates?: boolean | ((event: KeyboardEvent) => boolean);
/** Whether clicking outside deactivates the trap */
clickOutsideDeactivates?: boolean | ((event: MouseEvent | TouchEvent) => boolean);
/** Whether to allow clicks outside without deactivating */
allowOutsideClick?: boolean | ((event: MouseEvent | TouchEvent) => boolean);
/** Function to determine forward tab events (default: TAB) */
isKeyForward?: (event: KeyboardEvent) => boolean;
/** Function to determine backward tab events (default: SHIFT+TAB) */
isKeyBackward?: (event: KeyboardEvent) => boolean;
}
type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean;
type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean;Usage Examples:
// Basic interaction options
const trap1 = createFocusTrap('#modal', {
escapeDeactivates: true, // Escape closes (default)
clickOutsideDeactivates: true, // Click outside closes
});
// Conditional escape handling
const trap2 = createFocusTrap('#important-dialog', {
escapeDeactivates: (event) => {
// Only allow escape if not in critical state
return !document.querySelector('.unsaved-changes');
}
});
// Conditional outside click handling
const trap3 = createFocusTrap('#menu', {
clickOutsideDeactivates: (event) => {
// Don't close if clicking on menu trigger
return !event.target.closest('#menu-button');
}
});
// Allow specific outside clicks without closing
const trap4 = createFocusTrap('#sidebar', {
clickOutsideDeactivates: false,
allowOutsideClick: (event) => {
// Allow clicks on help tooltips
return event.target.closest('.help-tooltip');
}
});
// Custom navigation keys (arrow keys instead of tab)
const trap5 = createFocusTrap('#grid', {
isKeyForward: (event) => event.key === 'ArrowDown',
isKeyBackward: (event) => event.key === 'ArrowUp'
});Advanced configuration for specialized use cases and performance optimization.
interface AdvancedOptions {
/** Prevent scrolling when focusing elements */
preventScroll?: boolean;
/** Delay initial focus to next execution frame */
delayInitialFocus?: boolean;
/** Document context for the trap (for iframes) */
document?: Document;
/** Tabbable library options */
tabbableOptions?: FocusTrapTabbableOptions;
/** Shared trap stack for coordination */
trapStack?: Array<FocusTrap>;
/** Check if trap can receive focus (for animations) */
checkCanFocusTrap?: (containers: Array<HTMLElement | SVGElement>) => Promise<void>;
/** Check if focus can be returned (for animations) */
checkCanReturnFocus?: (trigger: HTMLElement | SVGElement) => Promise<void>;
}
interface FocusTrapTabbableOptions {
displayCheck?: 'full' | 'legacy-full' | 'non-zero-area' | 'none';
getShadowRoot?: (node: Element) => ShadowRoot | boolean;
}Usage Examples:
// Prevent scroll on focus
const trap1 = createFocusTrap('#modal', {
preventScroll: true // Don't scroll when focusing elements
});
// Iframe context
const iframe = document.querySelector('iframe');
const trap2 = createFocusTrap('#iframe-content', {
document: iframe.contentDocument
});
// Animation-aware focusing
const trap3 = createFocusTrap('#animated-modal', {
checkCanFocusTrap: async (containers) => {
// Wait for modal animation to complete
await new Promise(resolve => {
const modal = containers[0];
modal.addEventListener('animationend', resolve, { once: true });
});
},
checkCanReturnFocus: async (trigger) => {
// Wait for trigger to be visible again
await waitForElementVisible(trigger);
}
});
// Shared trap stack
const globalTrapStack = [];
const trap4 = createFocusTrap('#modal1', { trapStack: globalTrapStack });
const trap5 = createFocusTrap('#modal2', { trapStack: globalTrapStack });
// Shadow DOM support
const trap6 = createFocusTrap('#web-component', {
tabbableOptions: {
getShadowRoot: (node) => {
// Return shadow root for web components
return node.shadowRoot;
}
}
});
// Custom display check for performance
const trap7 = createFocusTrap('#large-container', {
tabbableOptions: {
displayCheck: 'none' // Skip display checks for performance
}
});All configuration options combined in the main Options interface:
interface Options {
// Lifecycle callbacks
onActivate?: () => void;
onPostActivate?: () => void;
onDeactivate?: () => void;
onPostDeactivate?: () => void;
onPause?: () => void;
onPostPause?: () => void;
onUnpause?: () => void;
onPostUnpause?: () => void;
// Focus targets
initialFocus?: FocusTargetOrFalse | undefined | (() => void);
fallbackFocus?: FocusTarget;
returnFocusOnDeactivate?: boolean;
setReturnFocus?: FocusTargetValueOrFalse | ((previousActiveElement: HTMLElement | SVGElement) => FocusTargetValueOrFalse);
// User interaction
escapeDeactivates?: boolean | KeyboardEventToBoolean;
clickOutsideDeactivates?: boolean | MouseEventToBoolean;
allowOutsideClick?: boolean | MouseEventToBoolean;
isKeyForward?: KeyboardEventToBoolean;
isKeyBackward?: KeyboardEventToBoolean;
// Advanced options
preventScroll?: boolean;
delayInitialFocus?: boolean;
document?: Document;
tabbableOptions?: FocusTrapTabbableOptions;
trapStack?: Array<FocusTrap>;
checkCanFocusTrap?: (containers: Array<HTMLElement | SVGElement>) => Promise<void>;
checkCanReturnFocus?: (trigger: HTMLElement | SVGElement) => Promise<void>;
}Understanding the default behavior when options are not specified:
// These are the effective defaults
const defaultOptions = {
returnFocusOnDeactivate: true,
escapeDeactivates: true,
delayInitialFocus: true,
clickOutsideDeactivates: false,
allowOutsideClick: false,
preventScroll: false,
document: window.document,
// All other options default to undefined
};Using focus-trap within iframe contexts requires passing the iframe's document:
// Setup trap inside iframe
const iframe = document.querySelector('#embedded-iframe');
const iframeDocument = iframe.contentDocument;
const trap = createFocusTrap('#iframe-modal', {
document: iframeDocument,
// Other options work normally within iframe context
initialFocus: '#iframe-input'
});
// Activate from parent document
iframe.contentWindow.addEventListener('load', () => {
trap.activate();
});For web components and shadow DOM elements:
// Custom element with shadow DOM
class ModalComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div class="modal">
<input type="text" />
<button>Close</button>
</div>
`;
this.trap = createFocusTrap(this.shadowRoot.querySelector('.modal'), {
tabbableOptions: {
getShadowRoot: (node) => {
// Enable shadow DOM traversal
return node.shadowRoot;
}
}
});
}
}Coordinating with CSS animations and transitions:
const animatedTrap = createFocusTrap('#animated-dialog', {
checkCanFocusTrap: async (containers) => {
// Wait for fade-in animation to complete
const dialog = containers[0];
return new Promise(resolve => {
const onAnimationEnd = () => {
dialog.removeEventListener('animationend', onAnimationEnd);
resolve();
};
dialog.addEventListener('animationend', onAnimationEnd);
});
},
checkCanReturnFocus: async (trigger) => {
// Wait for element to be visible again
return new Promise(resolve => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
observer.disconnect();
resolve();
}
});
observer.observe(trigger);
});
}
});Using arrow keys instead of Tab for navigation:
// Grid-style navigation with arrow keys
const gridTrap = createFocusTrap('#data-grid', {
// Override default tab behavior
isKeyForward: (event) => {
return event.key === 'ArrowRight' || event.key === 'ArrowDown';
},
isKeyBackward: (event) => {
return event.key === 'ArrowLeft' || event.key === 'ArrowUp';
},
// Still allow Escape to exit
escapeDeactivates: true
});
// Menu navigation with arrow keys
const menuTrap = createFocusTrap('#dropdown-menu', {
isKeyForward: (event) => event.key === 'ArrowDown',
isKeyBackward: (event) => event.key === 'ArrowUp',
// Close on outside click but allow menu trigger
clickOutsideDeactivates: (event) => {
return !event.target.closest('#menu-button');
}
});