Skip to content
DevDepth
← Back to all articles

In-Depth Article

Do React Portals Bubble Events to Parent Components? Why Parent onClick Still Fires

Yes for React synthetic events. No for native parent DOM listeners. Learn why Portal events follow the React tree, how Fiber ancestry matters, and why the DOM tree behaves differently.

Published: Updated: 6 min readreact-internals

Yes. React Portal events do bubble to parent React components.

More precisely:

  • yes for React synthetic events such as onClick
  • no for native listeners attached to the parent DOM node

That can look strange at first because the real DOM nodes rendered by a Portal may sit far away from the parent's DOM subtree, often directly under document.body. From the browser's DOM point of view, those nodes are physically separated.

But React does not decide synthetic event bubbling from the DOM tree alone. It decides it from the React tree.

That is the key idea to keep in your head:

A Portal changes where DOM nodes are mounted physically, but it does not break the child-to-parent relationship inside React's logical tree.

So if a parent component renders a Portal child, a click inside that Portal can still trigger the parent's onClick.

If you want adjacent context first, this article pairs well with how JSX becomes DOM in React, how React updates move from setState to a DOM update, and React diff algorithm: how reconciliation uses type, key, and lastPlacedIndex.

1. Start with the real contradiction: DOM tree vs React tree

To understand Portal bubbling, you have to separate two different structures.

The DOM tree

This is the browser's physical node hierarchy.

If a Portal renders into document.body, the browser may see something like this:

body
  -> app-root
  -> modal-root
      -> button

In native DOM terms, a click on that button bubbles upward through the DOM ancestors inside modal-root, then to body, document, and window.

It does not bubble through the parent's DOM element if that parent lives under app-root.

The React tree

Inside React, the logical relationship can still look like this:

ParentFiber
  -> PortalFiber
      -> ButtonFiber

From React's point of view, the Portal child still belongs to the component that rendered it.

That is why the Portal behavior feels surprising:

  • physically, the DOM moved
  • logically, the React parent-child relationship stayed the same

Portal event bubbling follows the second structure.

2. A minimal example that shows the behavior

Here is the familiar shape:

import { createPortal } from "react-dom";

function PortalChild() {
  return createPortal(
    <button onClick={() => console.log("portal child click")}>
      Click me
    </button>,
    document.body
  );
}

export default function Parent() {
  return (
    <div onClick={() => console.log("parent click")}>
      <PortalChild />
    </div>
  );
}

If you click the button, the usual React-side result is:

  1. the button's onClick runs
  2. the parent's onClick also runs

Even though the button is mounted under document.body, React still treats it as part of the parent's logical subtree.

3. Why this happens: React collects listeners by Fiber ancestry

The most important internal detail is this:

React does not build its synthetic bubbling path by walking domNode.parentNode alone.

Instead, once React resolves the target instance for the event, it walks the Fiber ancestry to collect listeners.

A simplified mental model looks like this:

let node = targetFiber;

while (node !== null) {
  collectListeners(node);
  node = node.return;
}

That return pointer is the parent link in the Fiber tree.

So when the target lives inside a Portal:

  • React finds the target Fiber for the clicked DOM node
  • React climbs node.return
  • it passes through the Portal boundary
  • it continues to the parent component Fiber
  • it collects the parent's onClick

That is the real reason the parent still receives the event.

4. What the Portal actually changes, and what it does not

A Portal changes the host container used for DOM insertion.

It does not rewrite the child into a separate React application with unrelated ancestry.

That means several things still work across the Portal boundary:

  • context still flows from the parent tree
  • synthetic events still bubble through the parent tree
  • React still treats the Portal subtree as part of the same logical ownership graph

So the safe mental model is:

A Portal relocates host nodes, not component ownership.

This is also why the official docs describe a Portal as something that changes physical placement but still behaves like a normal child in the React tree.

5. A source-level view: target DOM node first, Fiber traversal second

At the implementation level, the event flow is roughly:

  1. a native browser event happens on the Portal DOM node
  2. React's delegated event listener receives that native event
  3. React resolves the closest Fiber instance for the target DOM node
  4. React traverses upward through Fiber ancestors to collect matching listeners
  5. React dispatches a synthetic event through the collected path

Two source-level details matter here:

  • React resolves the target instance from the DOM node with logic such as getClosestInstanceFromNode(...)
  • the event system in DOMPluginEventSystem.js handles Portal boundaries while traversing Fiber ancestors

That is why a Portal can be physically outside the parent DOM subtree without losing logical bubbling to the parent component.

6. Why native DOM listeners behave differently

This is the mistake that causes a lot of confusion:

React synthetic bubbling and native DOM bubbling are not the same path.

If you attach a native listener directly to the parent DOM node, the Portal click will usually not reach that listener through normal DOM ancestry, because the Portal DOM node is not actually inside that parent's DOM subtree.

Here is the contrast:

import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";

function PortalChild() {
  return createPortal(<button>Click me</button>, document.body);
}

export default function Parent() {
  const parentRef = useRef(null);

  useEffect(() => {
    const node = parentRef.current;
    const handleNativeClick = () => console.log("native parent click");

    node.addEventListener("click", handleNativeClick);
    return () => node.removeEventListener("click", handleNativeClick);
  }, []);

  return (
    <div
      ref={parentRef}
      onClick={() => console.log("react parent click")}
    >
      <PortalChild />
    </div>
  );
}

When you click the Portal button:

  • the React onClick on the parent can fire
  • the native addEventListener("click", ...) on the parent DOM node usually will not fire through normal bubbling

That difference exists because the two systems are following different trees:

  • native listeners follow the DOM tree
  • React synthetic listeners follow the React ownership tree

7. How Portal boundaries fit into Fiber

Inside Fiber, a Portal is represented as a special node type rather than a fully detached tree with no parent.

Conceptually, you can picture it like this:

ParentFiber
  -> HostPortal
      -> ChildFiber

That is enough to explain the behavior:

  • the DOM container changes
  • the Fiber ancestry remains connected

So when React collects listeners, the Portal boundary is not a dead end. It is just another step in the logical tree.

8. What happens if you call stopPropagation inside the Portal

This is the practical question that usually follows.

If you call e.stopPropagation() on the React synthetic event inside the Portal child, React stops synthetic propagation through the React listener path.

That means the parent component's React onClick can be prevented even though the Portal remains logically connected.

For example:

function PortalChild() {
  return createPortal(
    <button
      onClick={(e) => {
        e.stopPropagation();
        console.log("portal child click");
      }}
    >
      Click me
    </button>,
    document.body
  );
}

That is often the fix when a modal, dropdown, or tooltip rendered with a Portal accidentally triggers an outer React click handler.

9. Common real-world cases where this matters

Portal bubbling is not just trivia. It shows up in real UI bugs.

Typical examples:

  • a modal rendered with a Portal triggers a parent card's onClick
  • a dropdown menu rendered into document.body still triggers an outer React handler
  • an overlay closes because an ancestor React click handler still sees the event

In all of these cases, the immediate instinct is often:

"But the DOM isn't inside the parent."

That instinct is correct for native DOM bubbling, but incomplete for React's synthetic event model.

10. The easiest interview answer

If someone asks, "Do Portal events bubble to the parent component?", the strong answer is:

  1. Yes, React Portal events bubble to parent React components
  2. They do not bubble that way because of DOM ancestry
  3. They bubble that way because React collects event listeners through the Fiber tree
  4. A Portal changes host DOM placement, but it does not sever React parent-child ownership
  5. Native DOM listeners still follow the real DOM tree, so they behave differently

That answer is much stronger than saying only "Portals still bubble."

11. Final takeaway

React Portal bubbling works because React's event system is based on the logical component tree, not only on the physical DOM tree.

So the clean mental model is:

  • the browser decides native bubbling from DOM ancestry
  • React decides synthetic bubbling from Fiber ancestry
  • a Portal changes where DOM nodes live
  • a Portal does not break logical React parenthood

Once that clicks, Portal event behavior stops feeling magical. It becomes a straightforward consequence of React owning its own event-dispatch path on top of the browser's native event.

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-17

Contact the editor