Portal

<Portal> relocates its children to another DOM node — by default document.body, or a selector / element you pass as to. Use it when an ancestor's overflow, z-index, or transform would clip or bury UI that should float above the page: toasts, tooltips, menus, and custom overlays.

JSX
import { Portal } from "@opentf/web";

{open && (
  <Portal to="body">
    <div class="toast">Saved!</div>
  </Portal>
)}

On connect the runtime moves the portal's light-DOM children to the target; on unmount it removes them so custom-element disconnect and effect cleanups run. No manual DOM bookkeeping.

Output
Portal

The box is overflow: hidden. An inline overlay would be clipped —<Portal to="body"> relocates the modal to <body>.

Ancestor clips anything that stays inside.

Native dialog

For modals, <dialog> uses the browser top layer — no portal needed. Backdrop and Esc-to-close are built in.

Native dialog

Top layer, ::backdrop, Esc to close — no relocation.

Open the portaled modal inside the clipped box — it renders on document.body, not inside the clip region. Compare with the native <dialog> below the demo.

The to target

to valueResolves to
omitteddocument.body
"body" / "#root" / any selectordocument.querySelector(…), falling back to body
a DOM Elementthat element directly
JSX
<Portal to="#toast-root">
  <p>Rendered into the element with id="toast-root".</p>
</Portal>

Portals vs native <dialog>

For modals and dialogs, the browser top layer is often simpler — no relocation, free backdrop, Esc-to-close:

JSX
const dialogRef = $ref();

<button onclick={() => dialogRef.showModal()}>Open</button>
<dialog ref={dialogRef}>
  <p>Native top layer</p>
  <button onclick={() => dialogRef.close()}>Close</button>
</dialog>

Reach for <Portal> when you need a general escape hatch — stacked toasts, custom overlays, tooltips anchored to arbitrary targets, or markup that does not map cleanly to <dialog>.

SSR and hydration

On first paint the portal defers relocation until hydration finishes so slotted content can adopt server-rendered DOM in place. On a client-only build the move runs immediately. See Hydration.

Runtime API

ExportDescription
PortalBuilt-in custom element — use as <Portal to={…}>…</Portal> in JSX.

Also listed on the Runtime API reference.

  • Context — portaled subtrees still resolve $context from the logical tree.

  • Error boundaries — throws inside portaled content route to the nearest boundary.

  • Components — where portals fit in the tree.

  • Imperative API$ref for native <dialog>.