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.
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:
- the button's
onClickruns - the parent's
onClickalso 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.parentNodealone.
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:
- a native browser event happens on the Portal DOM node
- React's delegated event listener receives that native event
- React resolves the closest Fiber instance for the target DOM node
- React traverses upward through Fiber ancestors to collect matching listeners
- 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.jshandles 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
onClickon 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.bodystill 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:
- Yes, React Portal events bubble to parent React components
- They do not bubble that way because of DOM ancestry
- They bubble that way because React collects event listeners through the Fiber tree
- A Portal changes host DOM placement, but it does not sever React parent-child ownership
- 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