useFocusTrap
Trap keyboard focus within an element. Essential for accessible modals, dialogs, and menus.
Live Demo
⌨️ Keyboard Navigation Test:
- Open a modal and press Tab to cycle through focusable elements
- Press Shift+Tab to cycle backwards
- Notice how focus stays trapped within the modal
- Try to focus elements outside the modal - you can't!
📝 Event Log
No events yet. Open a modal to test focus trapping!
Code Examples
Basic Modal with Focus Trap
import { useFocusTrap } from "@uiblock/hooks";
function Modal({ isOpen, onClose }) {
const trapRef = useFocusTrap({ enabled: isOpen });
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div ref={trapRef} className="modal-content">
<h2>Modal Title</h2>
<input placeholder="First input" />
<input placeholder="Second input" />
<button onClick={onClose}>Close</button>
</div>
</div>
);
}With Options
import { useFocusTrap } from "@uiblock/hooks";
function Dialog({ isOpen, onClose }) {
const trapRef = useFocusTrap({
enabled: isOpen,
autoFocus: true, // Focus first element on mount
restoreFocus: true, // Restore focus on unmount
allowEscape: false // Prevent Escape key from breaking trap
});
if (!isOpen) return null;
return (
<div ref={trapRef}>
<h2>Dialog</h2>
<button>Action 1</button>
<button>Action 2</button>
<button onClick={onClose}>Close</button>
</div>
);
}Accessible Form Dialog
import { useFocusTrap } from "@uiblock/hooks";
function FormDialog({ isOpen, onSubmit, onCancel }) {
const trapRef = useFocusTrap({ enabled: isOpen });
if (!isOpen) return null;
return (
<div ref={trapRef} role="dialog" aria-modal="true">
<h2>Enter Information</h2>
<form onSubmit={onSubmit}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<button type="submit">Submit</button>
<button type="button" onClick={onCancel}>Cancel</button>
</form>
</div>
);
}How It Works
Here's the implementation:
import { useEffect, useRef } from 'react'
const FOCUSABLE_ELEMENTS = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ')
export function useFocusTrap(options = {}) {
const { enabled = true, autoFocus = true, restoreFocus = true, allowEscape = false } = options
const ref = useRef(null)
const previouslyFocusedElement = useRef(null)
useEffect(() => {
if (!enabled) return
const element = ref.current
if (!element) return
// Store previously focused element
previouslyFocusedElement.current = document.activeElement
// Get all focusable elements
const getFocusableElements = () => {
if (!element) return []
return Array.from(element.querySelectorAll(FOCUSABLE_ELEMENTS))
}
// Focus first element if autoFocus is enabled
if (autoFocus) {
const focusableElements = getFocusableElements()
if (focusableElements.length > 0) {
focusableElements[0].focus()
}
}
// Handle tab key
const handleKeyDown = (event) => {
if (event.key !== 'Tab' && (!allowEscape || event.key !== 'Escape')) {
return
}
if (allowEscape && event.key === 'Escape') {
return
}
const focusableElements = getFocusableElements()
if (focusableElements.length === 0) return
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
// Shift + Tab
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault()
lastElement.focus()
}
}
// Tab
else {
if (document.activeElement === lastElement) {
event.preventDefault()
firstElement.focus()
}
}
}
element.addEventListener('keydown', handleKeyDown)
return () => {
element.removeEventListener('keydown', handleKeyDown)
// Restore focus to previously focused element
if (restoreFocus && previouslyFocusedElement.current) {
previouslyFocusedElement.current.focus()
}
}
}, [enabled, autoFocus, restoreFocus, allowEscape])
return ref
}Key Features:
- Traps focus within the container element
- Auto-focuses first element on mount
- Restores focus to previous element on unmount
- Handles Tab and Shift+Tab navigation
- Essential for accessible modals and dialogs
API Reference
Options
enabled?: booleanWhether the focus trap is enabled (default: true)
autoFocus?: booleanFocus the first element on mount (default: true)
restoreFocus?: booleanRestore focus to previously focused element on unmount (default: true)
allowEscape?: booleanAllow Escape key to break the trap (default: false)
returns: RefObjectRef to attach to the container element
💡 Use Cases
- Modal dialogs (accessibility requirement)
- Dropdown menus
- Alert dialogs
- Form wizards
- Sidebar navigation
- Any overlay that requires keyboard navigation