The clipping problem
Imagine a modal defined inside a card with overflow: hidden and
position: relative. Even though in React the modal is a "child" of the card, in the
DOM it would be clipped by those styles and trapped in its stacking
context (z-index). It's the classic case of a dropdown that gets cut off or a
modal that appears behind another element.
React.createPortal
A Portal lets you render some children in another DOM node, outside the parent component's visual hierarchy, while still belonging to the React tree:
import { createPortal } from "react-dom";
function Modal({ children }) {
return createPortal(
<div className="modal-overlay">{children}</div>,
document.body // mounted directly in <body>
);
}
createPortal(children, domNode) takes what to render and where (an
existing DOM node, typically document.body or a <div id="modal-root">).
The key: the DOM moves, not the React tree
Even though the modal appears in <body>, it's still a child of Modal in the
React tree. This has two important consequences:
- Context is preserved: the ported component reads the same
Context, props and state as if it were "in its place". It doesn't lose its connection with the app. - Events bubble through the React tree, not through the DOM. An
onClickinside the portal will bubble up to theModal's React parents, even though in the DOM it's on another branch.
Typical use cases
- Modals and dialogs: they mount in
<body>to cover the whole screen. - Tooltips and floating menus: they avoid being clipped by
overfloworz-index. - Notifications (toasts): they live in their own layer above everything.
Environment note: validating
createPortalisn't reliable in the test runtime, so in this module we work with it conceptually.