JAVASCRIPT
Implement Keyboard Focus Trap for Modals and Dialogs
Create `useFocusTrap`, a critical React hook for accessibility that restricts keyboard focus within a specified container, preventing users from tabbing outside of modals or dialogs.
import React, { useEffect, useRef, useCallback } from 'react';
const useFocusTrap = () => {
const containerRef = useRef(null);
const handleKeyDown = useCallback((event) => {
if (event.key === 'Tab' && containerRef.current) {
const focusableElements = containerRef.current.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (!focusableElements.length) {
event.preventDefault(); // If no focusable elements, prevent tab from leaving
return;
}
if (event.shiftKey) { // Shift + Tab
if (document.activeElement === firstElement || document.activeElement === containerRef.current) {
lastElement.focus();
event.preventDefault();
}
} else { // Tab
if (document.activeElement === lastElement) {
firstElement.focus();
event.preventDefault();
}
}
}
}, []);
useEffect(() => {
if (containerRef.current) {
// Set initial focus to the container or first focusable element
const focusableElements = containerRef.current.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
focusableElements[0].focus();
} else {
containerRef.current.focus(); // Fallback if no focusable children
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}
}, [handleKeyDown]);
return containerRef;
};
// Example Usage:
// function MyModal({ isOpen, onClose }) {
// const modalRef = useFocusTrap();
//
// if (!isOpen) return null;
//
// return (
// <div
// ref={modalRef}
// tabIndex="-1" // Make the container focusable
// aria-modal="true"
// role="dialog"
// style={{
// position: 'fixed',
// top: '50%',
// left: '50%',
// transform: 'translate(-50%, -50%)',
// border: '1px solid black',
// padding: '20px',
// background: 'white',
// zIndex: 1000,
// }}
// >
// <h2>Modal Title</h2>
// <p>This is a modal content.</p>
// <input type="text" placeholder="First input" />
// <button>A Button</button>
// <a href="#">A Link</a>
// <input type="text" placeholder="Second input" />
// <button onClick={onClose}>Close Modal</button>
// </div>
// );
// }
// function App() {
// const [isModalOpen, setIsModalOpen] = useState(false);
// return (
// <div>
// <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
// <p>Some content outside the modal.</p>
// <MyModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
// </div>
// );
// }
How it works: The `useFocusTrap` hook is essential for accessibility, particularly for modals, dialogs, and other overlays where keyboard focus must be contained. It returns a `ref` that should be attached to the container element you want to trap focus within. The hook listens for `Tab` key presses and programmatically redirects focus to loop within the container's focusable elements, preventing users from tabbing out of the component. It also attempts to set initial focus when the component mounts.