Portals
Rendering children into a different DOM node while keeping them in the React tree so events bubble normally.
The core idea#
createPortal(children, domNode) renders children into any DOM node outside the parent component's DOM hierarchy, while the component stays exactly where it is in the React tree. Perfect for modals, tooltips, dropdowns, and toasts that must escape a parent's overflow: hidden, z-index, or transform stacking context.
import { createPortal } from "react-dom";
function Modal({ children, onClose }) {
return createPortal(
<div className="overlay" onClick={onClose}>
<div className="dialog" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById("modal-root") // renders here, outside #app
);
}
Your index.html has a sibling mount node:
<body>
<div id="app"></div>
<div id="modal-root"></div>
</body>
Events bubble through the React parent, not the DOM parent 🎯#
This is the key mental model. Even though the modal's DOM lives under #modal-root, its synthetic events propagate through the React component tree. A click inside the portal bubbles up to ancestors that wrap <Modal/> in JSX — not to #modal-root's DOM ancestors.
function Toolbar() {
// This onClick fires when a button *inside the portal* is clicked,
// because React event bubbling follows the React tree.
return (
<div onClick={() => console.log("caught from portal child")}>
<Modal>
<button>Click me</button>
</Modal>
</div>
);
}
| Aspect | Follows |
|---|---|
| DOM position / CSS stacking | The target DOM node (#modal-root) |
| React context, state, event bubbling | The React parent tree |
⚠️ Because events bubble to React ancestors, a portal click can trigger handlers on elements it visually sits above. Use stopPropagation deliberately, and manage focus/Escape/scroll-lock yourself — portals don't add accessibility behavior.
flowchart TD
A["<Modal/> in React tree under Toolbar"] --> B["createPortal(children, #modal-root)"]
B --> C["DOM: children mounted inside #modal-root"]
C --> D["Click event on portal child"]
D --> E["Bubbles through REACT parent (Toolbar), not DOM parent"]
Interview Q&A#
Q1. What problem do portals solve?
They let a component render its DOM output into a node outside its parent's DOM subtree, escaping clipping (overflow: hidden), z-index traps, and transform stacking contexts — while the component remains logically inside the React tree.
Q2. If a portal renders elsewhere in the DOM, where do its events bubble?
Through the React component tree, not the DOM tree. A handler on a React ancestor of <Modal/> still catches events from the portal's children, even though those children live in a detached DOM node.
Q3. Does a portal component keep access to context and state?
Yes. It stays in the React tree, so useContext, props, and state work normally. Only the physical DOM insertion point changes.
Q4. What must you handle manually with modals via portals?
Accessibility and UX concerns React doesn't provide: focus trapping, restoring focus on close, Escape to dismiss, aria roles, and body scroll-locking. The portal only relocates DOM output.