patternjavascriptreactTip
React portals for modals, tooltips, and overlays outside the DOM hierarchy
Viewed 0 times
React 16+
createPortalportalmodaltooltipoverlayz-indexoverflow hiddenDOM escape
browser
Problem
A modal or tooltip rendered inside a deeply nested component inherits overflow: hidden, z-index stacking contexts, and other CSS constraints from its ancestors. The visual result appears clipped or behind other elements even if z-index is set high.
Solution
Use createPortal to render children into a DOM node outside the React tree:
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-panel" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body // renders here in the DOM
);
}
// Usage — Modal is inside a Card but renders at body level in the DOM
function Card() {
const [open, setOpen] = useState(false);
return (
<div style={{ overflow: 'hidden' }}> {/ doesn't clip the modal /}
<button onClick={() => setOpen(true)}>Open</button>
<Modal isOpen={open} onClose={() => setOpen(false)}>
<p>I escape the overflow!</p>
</Modal>
</div>
);
}
// Create a dedicated portal mount point in index.html
// <div id="modal-root"></div>
createPortal(content, document.getElementById('modal-root'));
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-panel" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body // renders here in the DOM
);
}
// Usage — Modal is inside a Card but renders at body level in the DOM
function Card() {
const [open, setOpen] = useState(false);
return (
<div style={{ overflow: 'hidden' }}> {/ doesn't clip the modal /}
<button onClick={() => setOpen(true)}>Open</button>
<Modal isOpen={open} onClose={() => setOpen(false)}>
<p>I escape the overflow!</p>
</Modal>
</div>
);
}
// Create a dedicated portal mount point in index.html
// <div id="modal-root"></div>
createPortal(content, document.getElementById('modal-root'));
Why
Portals render the React subtree's output into any DOM node while keeping the React component hierarchy intact. Events bubble through the React tree (not the DOM tree), so context and event handlers work normally. This separates CSS containment from logical component structure.
Gotchas
- Events bubble up through the React tree, not the DOM tree — a click inside a portal still bubbles to the React parent
- Portals don't affect context — the portal's children can still read context from their React parent
- In Next.js App Router, document is not available during SSR — guard with typeof document !== 'undefined'
- Accessibility: focus management and aria-modal are your responsibility with portals
Code Snippets
Tooltip using createPortal
import { createPortal } from 'react-dom';
function Tooltip({ text, children }) {
const [show, setShow] = useState(false);
return (
<span onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
{children}
{show && createPortal(
<div className="tooltip">{text}</div>,
document.body
)}
</span>
);
}Context
When building modals, tooltips, dropdowns, or notifications that must visually escape their parent's CSS context
Revisions (0)
No revisions yet.