Back to React
React
medium
mid

What are React Portals and when would you use them?

Portals render a child into a DOM node outside the parent's hierarchy while preserving React's component tree (state, context, events). Used for modals, tooltips, popovers, toasts — anything that needs to escape `overflow:hidden` or stacking context but stay logically inside the component. Events still bubble through React's virtual tree, not the DOM ancestor.

3 min read·~8 min to think through

What they are

tsx
import { createPortal } from 'react-dom';

function Modal({ children, open }) {
  if (!open) return null;
  return createPortal(
    <div className="modal">{children}</div>,
    document.body,
  );
}

The JSX renders into document.body, but in the React tree it's still a child of where <Modal> was used. Context, event delegation, and state all behave as if it's nested normally.

Why we need them

CSS stacking context and overflow are scoped:

html
<div style="overflow: hidden; position: relative; z-index: 0;">
  <Modal />   <!-- visually clipped or layered wrong -->
</div>

Portals escape: render to body (or a dedicated root container) so z-index and overflow are global.

When to use

  • Modals / dialogs.
  • Tooltips / popovers.
  • Toasts / notifications.
  • Dropdown menus that overflow their container.
  • Drag overlays.

What still works

  • React context (consumer in portal reads parent provider).
  • Event bubbling through React's synthetic system (clicks bubble up the virtual tree, not the DOM).
  • State and refs.
  • Suspense / error boundaries.

What doesn't

  • CSS inheritance via the DOM tree (since the actual ancestor is body, not the original parent).
  • Native event bubbling in the DOM ancestor.

A11y considerations

  • Focus trap inside dialogs (Radix Dialog, react-focus-lock).
  • aria-hidden on the rest of the page when modal is open.
  • role="dialog" + aria-modal="true" on the dialog element.
  • Restore focus to the trigger when closed.
  • Escape to close.

Adopt Radix Dialog / Headless UI Dialog — these are non-trivial to do right.

SSR

createPortal only works in the browser (needs document). Guard:

tsx
function Modal({ children }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return createPortal(<div>...</div>, document.body);
}

Dedicated portal root

Some apps create a dedicated root:

html
<div id="app"></div>
<div id="portals"></div>

Easier to clean up; predictable z-index stacking.

Anti-patterns

  • Portals for non-overlay UI (just use normal positioning).
  • DIY focus management instead of a library.
  • Multiple stacked modals fighting for focus.

Interview framing

"Portals render a child to a DOM node outside the parent hierarchy while preserving the React tree — context, state, and event bubbling work as if normally nested. Used for overlays: modals, tooltips, popovers, toasts, dropdown menus that overflow. Without them you fight overflow:hidden and z-index stacking. Pair with proper a11y: role="dialog", focus trap, escape to close, restore focus on close. Adopt Radix Dialog or Headless UI for the a11y — rolling your own focus management is a months-long project."

Follow-up questions

  • Why do events still bubble through the React tree?
  • What's a focus trap and why do dialogs need one?
  • When would you NOT use a portal?

Common mistakes

  • Skipping a11y (no focus trap, no aria).
  • Portals for non-overlay UI.
  • SSR breakage from createPortal without guard.

Performance considerations

  • No special cost. React reconciles the portal subtree normally.

Edge cases

  • Portals + Strict Mode double-render.
  • Nested portals.
  • CSS inheritance through portal boundary.

Real-world examples

  • Radix Dialog, Headless UI Dialog, react-toastify, MUI Modal.

Senior engineer discussion

Seniors adopt a11y-focused libraries for dialogs and reserve portals for genuine overlay needs.

Related questions