typescriptintermediate

Portal-Based Modal Component

Accessible modal component using React portals with focus trapping, Escape key close, and backdrop click.

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

interface ModalProps {
  open: boolean;
  onClose: () => void;
  children: ReactNode;
}

export function Modal({ open, onClose, children }: ModalProps) {
  const overlayRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;

    const handleKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', handleKey);
    document.body.style.overflow = 'hidden';

    contentRef.current?.focus();

    return () => {
      document.removeEventListener('keydown', handleKey);
      document.body.style.overflow = '';
    };
  }, [open, onClose]);

  if (!open) return null;

  return createPortal(
    <div
      ref={overlayRef}
      onClick={(e) => e.target === overlayRef.current && onClose()}
      style={{
        position: 'fixed', inset: 0, display: 'flex',
        alignItems: 'center', justifyContent: 'center',
        backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 50,
      }}
    >
      <div
        ref={contentRef}
        role="dialog"
        aria-modal="true"
        tabIndex={-1}
        style={{
          background: '#111', borderRadius: 12, padding: 24,
          maxWidth: 500, width: '90%', outline: 'none',
        }}
      >
        {children}
      </div>
    </div>,
    document.body
  );
}

// Usage:
// <Modal open={isOpen} onClose={() => setIsOpen(false)}>
//   <h2>Confirm</h2>
//   <button onClick={() => setIsOpen(false)}>Close</button>
// </Modal>

Use Cases

  • Confirmation dialogs
  • Image lightbox
  • Form overlays

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.