Ch. 3 · React

React Portals and Event Bubbling

Render outside the parent DOM with createPortal while events keep bubbling through the React tree, and manage focus and z-index.

~2 min readintermediateupdated Oct 5, 2026

A portal renders a child into a different DOM node, often document.body, while keeping it in the React tree. That solves stacking and overflow problems for modals and tooltips, but it changes what the DOM parent is, which affects events, focus and CSS.

Before you start

You should be comfortable with JSX, event handlers and the box model. This article focuses on portal behavior; accessibility details are covered separately.

Step-by-step walkthrough

Step 1: Choose the container

createPortal(children, container) needs a DOM node. document.body is common; a dedicated <div id="portal-root"> keeps portal content grouped and easier to style. Create the container once, outside render, so it is stable.

Step 2: Know that events follow the React tree

A click inside a portal still bubbles to its React ancestors, not to its DOM parent. A click handler on the component that owns the portal receives the event even though the DOM node lives elsewhere. This surprises people who reason from the DOM hierarchy.

Step 3: Handle focus and stacking deliberately

Portals reset stacking context, so a modal appears above everything, but focus is not moved automatically. Move focus into the dialog on open, trap it while open, and restore it to the trigger on close, or keyboard users lose their place.

Worked scenario

The modal renders into a portal but still bubbles clicks to its React parent.

import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="modal" role="dialog" aria-modal="true">
      {children}
      <button onClick={onClose}>Close</button>
    </div>,
    document.body,
  );
}
TSX

Walk through the example

When open is true, the modal’s DOM node is appended to document.body, so it escapes any overflow: hidden or stacking context of its React parent. Clicking Close calls onClose from the portal, which is a handler passed down through the React tree. The DOM parent changed, but the React parent did not.

Common mistake

Calling createPortal during a render without a stable container, which can create a new node each render, or assuming a click in a portal will not trigger the parent’s handler. Another is forgetting to move focus into the dialog, so screen readers and keyboards stay on the page behind it.

Verify the behavior

Click inside the portal and assert that an ancestor’s onClick still fires, proving React-tree bubbling. Inspect the DOM to confirm the node is under document.body. Tab through the open dialog and confirm focus is trapped and restored on close.

Interview exercise

Why does a click in a portal still trigger the parent component’s handler?

Answer and reasoning

React builds its own event system on the React tree, not the DOM tree. Portals keep their logical position in that tree even though their DOM node moved, so events propagate through the React ancestors as usual. Only the physical DOM location changed, which is why CSS and focus behave differently but event handlers do not.

Continue learning

Compare modal concerns in Accessible dialogs and Ref versus state. Read the React createPortal reference and try the React interview questions.

More in React

read ✓React · hard

React Context Splitting for Performance

Stop a single context value from re-rendering every consumer by splitting state and dispatch and memoizing the provider value.

~2 min readread →
read ✓React · mid

React lazy Loading and Code Splitting

Split bundles with lazy and Suspense, work with default exports, and place boundaries so a chunk loads only when needed.

~2 min readread →
esc