Portals
Learn how ReactDOM.createPortal() renders a component into a different part of the real DOM tree, and build a Modal that escapes its parent's overflow and stacking context.
Introduction
React normally renders every component exactly where it appears in the component tree — a child renders inside its parent's DOM node, which renders inside its parent, and so on. Usually that is exactly what you want. But some UI elements, like modals, tooltips, and dropdowns, need to visually break out of that structure.
Portals let you render a component's children into a completely different location in the real DOM, while keeping it in its original place in the React component tree. This lesson explains what portals are, why they solve real layout problems, and walks through building a Modal component with one.
- What a portal is and how it differs from normal rendering.
- Why modals, tooltips, and dropdowns often need portals.
- The syntax for ReactDOM.createPortal().
- A worked example of a Modal rendered into a #modal-root div.
- Why event bubbling still follows the React tree, not the DOM tree.
What is a Portal?
A portal is a way to render a child element into a DOM node that lives outside its parent component's DOM hierarchy. Normally, if <Modal /> is rendered inside a <div className="card">, the modal's DOM output ends up nested inside that card's DOM node too. A portal lets the modal's actual markup appear somewhere else in the document entirely — for example, directly under <body> — while React still treats it as a child of the component that rendered it.
Different DOM Location
The rendered output appears in a DOM node you specify, not inside its parent's DOM node.
Same React Tree Position
React still treats the portaled content as a normal child for context, state, and event purposes.
Escapes Layout Constraints
Content can visually escape a parent's overflow:hidden or a low z-index stacking context.
One Function Call
ReactDOM.createPortal() is the only API needed — no extra libraries required.
Why Portals Are Useful
Modals, tooltips, and dropdown menus are meant to visually float above everything else on the page. But if they are rendered deep inside a component with overflow: hidden or a parent with a lower z-index in a complex stacking context, the browser may clip them or draw them behind other elements — no matter how high you set their own z-index.
Portals sidestep this entirely by rendering the modal's markup directly into a dedicated DOM node near the top of the document, completely outside any clipping or stacking constraints imposed by its logical parent.
- Modal dialogs that must appear above every other element on the page.
- Tooltips and popovers that need to escape a scrollable or clipped container.
- Dropdown menus that would otherwise be cut off by a parent with overflow: hidden.
- Toast or notification banners rendered at the document root regardless of where they were triggered.
createPortal() Syntax
ReactDOM.createPortal() takes two arguments: the React children to render, and the real DOM node to render them into. It returns something you can return directly from a component's render output, just like any other JSX.
import { createPortal } from 'react-dom';
function Example() {
return createPortal(
<p>This renders into a different DOM node!</p>,
document.getElementById('modal-root')
);
}document.getElementById('modal-root') must already exist in the HTML page (usually a plain <div id="modal-root"></div> added next to your root div in index.html) before createPortal tries to render into it.
Example: A Modal Component
First, add a dedicated container to your HTML file for portaled content to render into, alongside the main app root.
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>Next, build a reusable Modal component that renders its children through a portal into that #modal-root div.
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
export default Modal;Now the Modal can be used anywhere in the app, even deep inside a card with overflow: hidden, and it will still render on top of everything else because its real DOM location is #modal-root, not the card.
import { useState } from 'react';
import Modal from './Modal';
function ProfileCard() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="card" style={{ overflow: 'hidden' }}>
<button onClick={() => setIsModalOpen(true)}>Edit Profile</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Edit Profile</h2>
<p>Update your details below.</p>
</Modal>
</div>
);
}
export default ProfileCard;<div id="root">
...ProfileCard markup, with overflow: hidden...
</div>
<div id="modal-root">
<div class="modal-overlay">...Edit Profile modal...</div>
</div>Event Bubbling Still Follows React
Even though the modal's DOM node lives outside ProfileCard in the actual document, React events triggered inside the modal still bubble up through the React component tree as if it were rendered right where <Modal> appears in the JSX. A click inside the modal will still trigger an onClick handler on ProfileCard, even though they are not DOM ancestors.
Key Behavior
- Event bubbling and context (like useContext) follow the React tree, not the physical DOM tree.
- This means portaled content still behaves predictably with parent event handlers and any context providers above it.
- Only the visual/DOM placement changes — the logical React relationship stays exactly the same.
Common Mistakes
- Forgetting to add the target DOM node (like #modal-root) to your HTML file before calling createPortal.
- Assuming a click inside the portal will not bubble up to parent React handlers — it still does.
- Not calling e.stopPropagation() on the modal content, causing clicks inside it to also trigger an overlay's onClose.
- Using a portal for every component "just in case" instead of only where DOM placement genuinely needs to differ.
Best Practices
- Reserve portals for cases where the DOM position genuinely needs to differ — modals, tooltips, dropdowns.
- Keep a single, well-known target node like #modal-root for all portaled content rather than creating many ad hoc ones.
- Always stop click propagation on modal content itself so clicking inside does not also trigger an overlay close.
- Remember that context and event bubbling still follow the component tree, so portals rarely break existing logic.
- Clean up any body scroll locks or focus traps when the portal-rendered modal closes.
Frequently Asked Questions
Do portals break React context?
No. Context still flows through the React component tree, not the DOM tree, so a portaled component can still read context from providers above it in the JSX.
Why do modals specifically need portals?
Modals need to visually sit above all other content, but if rendered inside a parent with overflow: hidden or a constrained stacking context, they can be clipped or hidden behind other elements. Portals let them render into an unconstrained DOM location instead.
Does createPortal change how events bubble?
No. Even though the DOM location changes, event bubbling still follows the React component tree, so parent onClick handlers still fire as expected.
Can I portal into any DOM node?
Yes, as long as that DOM node exists at the time createPortal runs. It is common to add a dedicated div, like #modal-root, directly in your HTML file.
Is a portal a separate React root?
No. It is still part of the same React tree and render — only its final DOM output location changes, not its lifecycle or state management.
Key Takeaways
- A portal renders a component's children into a different real DOM node than its parent, using ReactDOM.createPortal(child, domNode).
- Portals are ideal for modals, tooltips, and dropdowns that need to escape a parent's overflow or stacking context.
- The target DOM node (like #modal-root) must already exist in the HTML before createPortal renders into it.
- Event bubbling and context still follow the React component tree, not the physical DOM structure.
- Portals do not create a separate React root — they are still part of the same render and lifecycle.
Summary
Portals give React a clean way to separate where a component logically lives in your code from where its output actually appears in the browser's DOM. This solves real, common layout problems for anything that needs to visually float above the rest of the page.
In this lesson, you learned what a portal is, why modals and tooltips often need one, how to use ReactDOM.createPortal(), and that event bubbling still follows the React tree regardless of DOM placement. Next, you will learn how higher-order components let you reuse logic by wrapping one component with another.
- You understand what a portal is and how it differs from normal rendering.
- You know why modals, tooltips, and dropdowns often rely on portals.
- You can render a Modal component through a portal into #modal-root.
- You understand that event bubbling still follows the React component tree.