React / React Portal / Portals with useRef
Home /React /React Portal /Portals with useRef

Portals with useRef

Portals with useRef and Focus Management

Good modal UX requires focus trapping: when a modal opens, keyboard focus must
stay inside it. When it closes, focus should return to the trigger element.

Focus Trap Pattern


import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';

function AccessibleModal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Save who had focus before modal opened
      previousFocusRef.current = document.activeElement;

      // Move focus into the modal
      const focusable = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      if (focusable?.length) focusable[0].focus();
    } else {
      // Return focus to the trigger element
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  // Trap Tab key inside modal
  function handleKeyDown(e) {
    if (e.key !== 'Tab') return;

    const focusable = modalRef.current?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  }

  if (!isOpen) return null;

  return createPortal(
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      onKeyDown={handleKeyDown}
      style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}
    >
      <div style={{ background: '#fff', padding: '2rem', borderRadius: '8px' }}>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

export default AccessibleModal;