Skip to content

Portal

Renders its children at the end of the page or into another container, so that an overlay escapes the overflow and the stacking of its parents.

import { Portal } from '@contour-ui/headless-react';

<Portal>
  <div className="toast">Saved</div>
</Portal>;

Portal renders its children at the end of document.body, outside the element it sits in, so that an overlay, such as the content of a popover or of a dialog, is not cut off by a parent with overflow: hidden and does not end up under the content that comes after it. It renders no element of its own.

Only the DOM moves. In React, the children stay where the portal is: they read the context around it, and their events, such as a click, bubble up through the components around the portal, not through body.

Container

container renders the children into another element: the element itself, or a ref to it. A shadow root works too.

const layerRef = useRef<HTMLDivElement>(null);

<>
  <div ref={layerRef} className="layer" />
  <Portal container={layerRef}>…</Portal>
</>;

The portal reads the ref again after each of its commits, so the element may be rendered in the same commit as the portal, or later. While there is no element, with container={null} or a ref that is still empty, the portal renders nothing: it never falls back to document.body, where the children would mount, only to move once the container is there.

In place

disabled renders the children where the portal is, as if there were none: for content that is an overlay in one layout and part of the page in another.

<Portal disabled={inline}>
  <Menu />
</Portal>

Server rendering

On the server, Portal renders nothing, and so does the render that hydrates that markup in the browser: the markup matches, so React reports no mismatch. The children are rendered into the container right after hydration. A page rendered in the browser alone gets them at once, in the same commit. With disabled, the children are part of the markup, on the server and in the browser alike.

With Presence

Overlays combine the two: Portal puts the content at the end of the page, and Presence keeps it on screen while its exit animation runs.

<Portal>
  <Presence present={open} unmountOnExit className="toast">
    Saved
  </Presence>
</Portal>

Props

PropTypeDefault
containerElement | DocumentFragment | RefObject<Element | DocumentFragment | null> | nulldocument.body
disabledbooleanfalse
childrenReactNode—

Accessibility

At the end of body, the children come last in the reading order and in the Tab order of the page. An overlay that renders through a portal moves focus into its content when it opens, and back to the element that opened it when it closes, so that keyboard and screen reader users get there and back.