Portals
Render children into a different DOM node with createPortal: modals, tooltips, and the event bubbling surprise.
- react
- portals
A modal inside a overflow: hidden card gets clipped. A tooltip needs to escape a stacking context. Portals let a component render part of its tree into a different DOM node while keeping the same React parent for context and events.
import { createPortal } from 'react-dom';
function Modal({ children, onClose }) {
return createPortal(
<div className="modal-root" role="dialog" aria-modal="true">
<button type="button" onClick={onClose}>Close</button>
{children}
</div>,
document.getElementById('modal-root')
);
}
Docs: Portals.
React tree versus DOM tree
- React tree:
Modalis still a child ofCardfor context (theme,router) and for event bubbling in React’s system. - DOM tree: the nodes live under
#modal-root, often a direct child ofdocument.body.
// HTML
<body>
<div id="app">...</div>
<div id="modal-root"></div>
</body>
Events still bubble through React parents
Click events from portal content bubble to React ancestors above the portal host, not only DOM parents. That is usually what you want for “click outside” patterns implemented with React handlers higher up — and a footgun if you assumed DOM structure.
function App() {
return (
<div onClick={() => console.log('app')}>
<Modal>
<button type="button">Portal button</button>
</Modal>
</div>
);
}
// Clicking the portal button still triggers the app onClick in React
Accessibility requirements
Portals do not free you from dialog a11y:
role="dialog"+aria-modal="true"(or the native<dialog>).- Focus trap inside the modal while open.
- Restore focus to the opener on close.
Escapecloses.- Label with
aria-labelledby/aria-label.
SSR caution
document.getElementById does not exist on the server. Guard the portal target:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return createPortal(ui, document.body);
Or only render the modal after open on the client. Avoid hydration mismatches by not assuming the portal target in the server HTML for that subtree.
Interview out-loud
“createPortal renders children into a different DOM node while keeping React parent context and event bubbling. I use it for modals and tooltips that must escape overflow and stacking contexts, and I still implement focus management and labels for accessibility.”
Related on this site
Further reading
Production checklist
- Source of truth clear for every piece of UI state?
- Remount, route change, and Strict Mode cleanup paths handled?
- Urgent updates separated from deferrable work?
- Profiled before memo, virtualization, or context splits?
- Keyboard, focus, and accessible names still work after the change?
Edge cases worth rehearsing
Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.
Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.
When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.