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.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs