Lesson 27 of 31
Portals
Rendering a component's output into a different DOM node than where it lives in the tree — the classic fix for modals and tooltips escaping overflow and stacking-context traps.
Everything so far assumes a component's rendered output appears exactly
where it sits in the tree — a Modal rendered inside Dashboard produces
DOM nested inside Dashboard's own DOM. Usually that's exactly right.
Occasionally it's the problem.
The problem: clipping and stacking
CSS properties like overflow: hidden (to clip content within a card) or a
stacking context created by transform or z-index don't stop at
component boundaries — they apply to real DOM ancestry, regardless of how
the React tree is organized:
function Card() {
return (
<div style={{ overflow: "hidden", position: "relative" }}>
<p>Some card content</p>
<Modal>This gets visually clipped, no matter its own z-index</Modal>
</div>
);
}However high Modal's own z-index is set, if it's rendered as a normal
DOM descendant of an element with overflow: hidden, it gets clipped at
that ancestor's boundary. This is a real, common problem for anything meant
to visually "float above" the rest of the page — modals, tooltips,
dropdown menus.
The fix: createPortal
import { createPortal } from "react-dom";
function Modal({ children }) {
return createPortal(
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>,
document.getElementById("modal-root")
);
}createPortal(children, domNode) renders children into domNode — a
real DOM node that can live anywhere in the actual document, commonly a
dedicated container like #modal-root placed as a sibling to the app's main
root, specifically outside any ancestor that might clip or misorder it:
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>Modal still logically lives wherever it's rendered in the React tree —
inside Card, in the earlier example — but its actual DOM output now
appears as a child of #modal-root, entirely outside Card's DOM subtree
and whatever clipping it applies.
What still follows the React tree, and what doesn't
This is the detail worth being precise about: a portal changes where the DOM node lands, and nothing else about how the component behaves within React.
- Event bubbling follows the React tree, not the DOM tree. A click
inside a portaled modal still bubbles up through the React component
hierarchy the modal logically belongs to — so an
onClickonCard(the modal's logical parent) still fires for clicks inside the modal, even though the modal's actual DOM node is physically a sibling ofCard's DOM, somewhere else in the document entirely. - Context still flows normally. A portaled component reading
useContextstill sees whateverProviderwraps it in the React tree, regardless of where its DOM output physically renders. - Only the DOM placement changes — layout, clipping, and stacking are governed by the real DOM position, which is exactly the thing a portal exists to change.
Try it yourself
Notice the modal visually escapes the clipping card below, while a click inside it still triggers the outer handler — because event bubbling follows the React tree, not the DOM structure:
What to remember
- createPortal(children, domNode) renders a component's DOM output into a different real DOM node, without changing its logical position in the React tree.
- It's the standard fix for content — modals, tooltips, dropdowns — that needs to escape an ancestor's overflow clipping or stacking context.
- Event bubbling and context still follow the React tree, not the physical DOM position — only layout/clipping/stacking behavior is what actually changes.
- A dedicated DOM node (like #modal-root), placed outside the app's main container, is a common target for portaled content.
Check yourself
4 questions · pass 3/4 to unlock forwardRef and Ref as a Prop
1.What does createPortal actually do?
2.What problem do portals most commonly solve?
3.An onClick handler inside a portaled modal calls event.stopPropagation() partway up. Does a click handler on a React ancestor still see the event, even though the modal's actual DOM node lives outside that ancestor in the document?
4.Where does the DOM node passed to createPortal typically come from?
4 left to answer