What is Portal?
Portal: A portal renders part of a React tree into a different DOM node, usually document.body, so dialogs and popovers escape overflow clipping and stacking contexts.
createPortal(children, container) tells React to mount some elements under a different DOM parent while keeping them in the same React tree. The usual target is document.body. Overlays need this because CSS does not care where the React component sits: a popover rendered inside a card with overflow: hidden gets clipped, one inside a transformed element is positioned relative to that element rather than the viewport, and one inside a low z-index stacking context is covered by a sibling. Rendering at the end of body sidesteps all three.
What stays the same
- Context still flows from the React parent, so a portaled dialog reads the theme, the form state and the router as if it were inline.
- Events bubble through the React tree, not the DOM tree. A click inside a portaled popover reaches an
onClickon its React ancestor, which surprises code that uses a parent's handler to detect a click outside. - State and lifecycle are unchanged; the portal is only about where the nodes land.
What changes
- DOM order. The portaled content is last in the document, so Tab from the trigger would walk the whole page before reaching it. Overlays must move focus into themselves on open and back on close, and modal ones need a focus trap.
- Screen reader order follows the DOM too, which is why dialogs set
aria-modaland popovers return focus deliberately. - CSS inheritance from the original parent is lost. Styles that depended on a wrapper class, such as a dark section, must be applied to the portal content or the root.
- Server rendering. There is no
documenton the server, so a portal either renders after mount or uses a library component that handles it, which is what Base UI'sPortaldoes.
The top layer
Browsers now have a built-in answer to the stacking problem. An element opened with dialog.showModal() or marked with the HTML popover attribute is promoted to the top layer, above every z-index and outside every stacking context, without moving in the DOM, and this is supported in most browsers. It does not position an element next to its trigger by itself, so libraries still combine it with a portal or a positioning engine, but for a full-screen modal the native dialog element is a good fit.
How to build one in React
"use client"
import * as React from "react"
import { createPortal } from "react-dom"
export function BodyPortal({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => setMounted(true), [])
if (!mounted) return null
return createPortal(children, document.body)
}
The mounted flag keeps the server and the first client render identical, which avoids a hydration mismatch. MiniDev's Dialog, Popover, DropdownMenu and Tooltip each wrap their popup in a Base UI Portal, and the Toaster mounts once at the root so toasts are already outside every layout container. Browse the overlays category for the full set.