or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdindex.mdtrap-control.mdtrap-creation.md
tile.json

configuration.mddocs/

Configuration

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.

Capabilities

Lifecycle Callbacks

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');
  }
});

Focus Target Configuration

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
});

User Interaction Options

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 Options

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
  }
});

Complete Options Interface

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>;
}

Default Values

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
};

Advanced Usage Examples

Iframe Integration

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();
});

Shadow DOM Integration

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;
        }
      }
    });
  }
}

Animation-Aware Focus Management

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);
    });
  }
});

Custom Navigation Patterns

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');
  }
});