Use portals to render components outside the DOM hierarchy for modals and overlays.
import { createPortal } from "react-dom";
function Modal({ children }) {
const root = document.getElementById("modal-root");
return createPortal(
<div className="modal">{children}</div>,
root
);
}<div id="root"></div>
<div id="modal-root"></div>function Portal({ children, containerId = "portal-root" }) {
const container = document.getElementById(containerId);
if (!container) return null;
return createPortal(children, container);
}function Modal({ isOpen, onClose, children, title }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<h2>{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
document.getElementById("portal-root")
);
}
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal isOpen={open} onClose={() => setOpen(false)} title="Hello">
Modal content here
</Modal>
</>
);
}useEffect(() => {
if (!isOpen) return;
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusable?.[0]?.focus();
}, [isOpen]);useEffect(() => {
function handleKey(e) {
if (e.key === "Escape") onClose();
}
if (isOpen) document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}, [isOpen, onClose]);useEffect(() => {
document.body.style.overflow = isOpen ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [isOpen]);function Parent() {
const handleClick = (e) => {
if (e.target.closest(".modal")) {
console.log("Modal clicked");
}
};
return (
<div onClick={handleClick}>
<Modal>Content</Modal>
</div>
);
}function Modal({ onClose, children }) {
return createPortal(
<div className="backdrop" onClick={onClose}>
<div className="content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById("modal-root")
);
}function Tooltip({ content, children }) {
return (
<>
{children}
{createPortal(
<div className="tooltip">{content}</div>,
document.body
)}
</>
);
}function Dropdown({ isOpen, options }) {
if (!isOpen) return null;
return createPortal(
<ul className="dropdown">
{options.map(opt => <li key={opt}>{opt}</li>)}
</ul>,
document.body
);
}function Toast({ message }) {
return createPortal(
<div className="toast">{message}</div>,
document.getElementById("toast-container")
);
}function ContextMenu({ x, y, items, onClose }) {
return createPortal(
<ul className="context-menu" style={{ top: y, left: x }}>
{items.map(item => (
<li key={item.label} onClick={() => { item.action(); onClose(); }}>
{item.label}
</li>
))}
</ul>,
document.body
);
}function Lightbox({ src, alt, onClose }) {
return createPortal(
<div className="lightbox" onClick={onClose}>
<img src={src} alt={alt} />
</div>,
document.getElementById("portal-root")
);
}<body>
<div id="root"></div>
<div id="modal-root"></div>
<div id="tooltip-root"></div>
</body>useEffect(() => {
return () => {
// Cleanup if needed
};
}, []);.modal-backdrop {
z-index: 1000;
}useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus();
}
}, [isOpen]);createPortal(
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Confirm Delete</h2>
{children}
</div>,
document.getElementById("modal-root")
)Use ReactDOM.createPortal(element, document.body) to mount the modal directly on the body while keeping it inside your React tree. This means context providers and state from parent components remain accessible inside the portal.
Portal events bubble through the React component tree, not the DOM tree, so native document-level listeners won't fire for clicks inside a portal. Move event handling into React components using onClick or similar synthetic event handlers instead.
Portals are preferable when a parent component has overflow: hidden, transform, or z-index stacking context issues that would clip or obscure your overlay even with fixed positioning. Use position: fixed inside a portal for the cleanest combination of both.
Yes — portal children still belong to the React tree where createPortal is called, so Context, Redux state, and hooks all work normally even though the portal renders to a different DOM node.
createPortal keeps the component inside React's reconciliation cycle, giving you lifecycle methods, hooks, and automatic cleanup on unmount. Raw DOM manipulation bypasses React entirely, requiring manual cleanup and losing all React features.