React
Portals for modals and tooltips, without traps
createPortal moves the node into the body and keeps events and context in the React tree: the modal escapes CSS, not components.
Key points
- createPortal changes the destination DOM node, not the position in the React tree.
- An event inside the portal still bubbles through the parent components, not through the body nodes.
- Focus must move inside the modal on open and return to the trigger on close.
A portal renders a child into a different DOM node, usually document.body, and leaves context, state and React events intact. It exists to lift modals and tooltips out of a container with hidden overflow or a z-index that clips them.
What stays and what leaves
The node moves into the body; bubbling events follow the React tree, not the DOM. A click inside the modal still reaches the handler on the parent component, which surprises anyone expecting isolation.
The runnable case
import { createPortal } from "react-dom";
import { useEffect, useRef } from "react";
export function Modale({ aperta, onChiudi, children }) {
const ritorno = useRef(null);
useEffect(() => {
if (!aperta) return;
ritorno.current = document.activeElement;
const onKey = (e) => e.key === "Escape" && onChiudi();
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
ritorno.current?.focus();
};
}, [aperta, onChiudi]);
if (!aperta) return null;
return createPortal(
{children}
,
document.body,
);
}
The code moves the content into the body, closes on Escape and returns focus to the element that opened the modal. The focus trap is not included: for that, use a Radix or React Aria primitive, both under the MIT licence.
Tooltip: portal or not
A tooltip inside a container with hidden overflow gets clipped, and the portal saves it. The price is positioning: coordinates must be computed against the viewport and updated on scroll, work that Floating UI, MIT licence, already does.
Live records in the catalogue
The dialog category collects the modals in the catalogue, with the Origin UI dialog on Radix primitives; for short labels there is the tooltip category.
The limitation
A portal does not carry CSS with it: a style written under a container stops applying in the body, and CSS variables set on a wrapper never reach the moved node. Tokens belong on the root selector, not on an intermediate div.
Where to continue
The accessible component checklist is in accessible React components; the primitives are in Radix, Base UI and React Aria; the ARIA vocabulary is in what ARIA is. The React and Svelte comparison for content sites is in Svelte vs React.
Frequently asked questions
Does a portal break React context?
No. Context follows the component tree, so a provider value stays readable inside the portal even though the node sits in the body.
Why does my modal not pick up styles?
Because the node lives in the body and not under the container that defined the styles. Move the tokens to the root selector or add a class to the portal node.
Does a click outside the modal close it?
No. You need a listener on the document that compares the target with the modal node, and it must be removed on close.
Portal or native dialog?
The native dialog handles focus, Escape and the top layer without JavaScript. The portal earns its place when you need animation or finer control over the markup.
Can I use a portal for a dropdown?
Yes, but the menu must stay reachable by keyboard and close on Escape. A portal menu without focus management is worse than a clipped menu.