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 onClick on 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-modal and 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 document on the server, so a portal either renders after mount or uses a library component that handles it, which is what Base UI's Portal does.

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

tsx
"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.

DialoguiShadcn compatible modal dialog on Base UI with a blurred backdrop, enter and exit animation, close button, header, title, description and a footer bar.npx shadcn@latest add @minidev/dialogPopoveruiA floating React panel anchored to a trigger, built on Base UI with side and align options, collision aware positioning, title and description parts.npx shadcn@latest add @minidev/popover