typescriptintermediate
React Portal Modal Component
Build an accessible modal dialog using React portals with focus trapping and keyboard handling.
typescriptPress ⌘/Ctrl + Shift + C to copy
'use client';
import { useEffect, useRef, ReactNode } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
}
export function Modal({ isOpen, onClose, title, children }: ModalProps) {
const overlayRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
// Focus trap
if (e.key === 'Tab' && contentRef.current) {
const focusable = contentRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last?.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first?.focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
contentRef.current?.querySelector<HTMLElement>('button, input')?.focus();
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
ref={overlayRef}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
onClick={(e) => e.target === overlayRef.current && onClose()}
role="dialog"
aria-modal="true"
aria-label={title}
>
<div ref={contentRef} className="bg-[#111] rounded-xl p-6 max-w-lg w-full mx-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">{title}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-white">
✕
</button>
</div>
{children}
</div>
</div>,
document.body
);
}Use Cases
- Accessible modal dialogs with focus management
- Confirmation dialogs and detail overlays
- Rendering outside parent DOM hierarchy
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptintermediate
Portal-Based Modal Component
Accessible modal component using React portals with focus trapping, Escape key close, and backdrop click.
Best for: Confirmation dialogs
#modal#portal
typescriptbeginner
useKeyboardShortcut Hook
Register global keyboard shortcuts with modifier keys, preventing defaults, and cleanup on unmount.
Best for: Command palette shortcuts
#react#hooks
typescriptintermediate
Accessible Tabs Component
Build an accessible tabs component with keyboard navigation, ARIA attributes, and focus management.
Best for: Tab navigation interfaces
#react#accessibility
typescriptintermediate
Error Boundary with Fallback UI
Class-based error boundary component that catches render errors and displays a customizable fallback UI.
Best for: Graceful error recovery
#error-boundary#error-handling