Portals render outside the tree so overlays escape their parents
A portal renders a component’s DOM output somewhere else in the document — usually
a node at the end of <body> — while keeping the component exactly where it is in
the React tree. That split is the whole trick, and it exists to solve one stubborn
problem: overlays. A modal, a dropdown, a tooltip logically belongs to the
component that opened it (it needs that component’s props and state), but it must
escape that component’s DOM box to avoid being clipped by overflow: hidden or
buried by a z-index it cannot win. A portal lets an overlay keep its logical
parent and its React state while breaking out of its physical container.
The overflow and z-index trap
Without a portal, an overlay renders inside its parent’s DOM, and it inherits the
parent’s constraints. If any ancestor has overflow: hidden (extremely common on
cards, scroll areas, and tables), the overlay is clipped at the box edge. If an
ancestor establishes a stacking context with a lower z-index than a sibling
elsewhere, no z-index you set on the overlay can lift it above that sibling —
stacking contexts are not global. These are not bugs in your CSS; they are the
overlay being trapped in the wrong part of the tree:
/* extremely common — and it silently clips any overlay rendered inside */
.card {
overflow: hidden; /* rounds the corners… and crops the dropdown */
transform: translateZ(0); /* creates a stacking context: z-index is now local */
}
.card .dropdown { z-index: 9999; } /* still can't beat a sibling outside .card */
Render into a node outside the trap
createPortal takes the JSX and a target DOM node, and renders there instead of
in place. The component stays a normal child in React — it receives props, holds
state, and dispatches events up the React tree as usual:
import { createPortal } from "react-dom";
function Modal({ open, onClose, children }) {
if (!open) return null;
return createPortal(
<div className="overlay" onClick={onClose}>
<div className="modal" role="dialog" aria-modal="true">{children}</div>
</div>,
document.getElementById("portal-root") // renders at the end of <body>
);
}
The #portal-root sits at the top level of the DOM, free of any ancestor’s
overflow or stacking context, so the modal is never clipped and its z-index
behaves globally.
Events still follow the React tree, and a11y still needs care
The detail that surprises people: because the component stays in the React tree,
events bubble through the React parent, not the DOM parent. A click inside the
portal reaches an onClick on the logical parent component even though the DOM node
lives elsewhere — which is usually what you want, and occasionally a gotcha if you
relied on DOM-based bubbling. The portal solves placement, not accessibility: a
portalled modal still needs focus moved into it, focus trapped, Escape to close, and
focus restored on close — the DOM location does not do any of that for you. Use
portals for exactly the things that must escape their container — modals,
dropdowns, tooltips, toasts — and pair them with the focus choreography an overlay
requires. The accessible-combobox exercise combines both: a listbox that must
escape its input’s overflow and manage focus correctly, which is the portal
pattern at full strength.