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/afw";

{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. A custom overlay has to supply what <dialog> gives for free, and the demo does: role="dialog" with aria-modal and a labelled title, focus moved inside on open and kept there with Tab, Escape and backdrop clicks to close, the page behind made inert, and focus returned to the opening button. 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>.

Last updated on
Edit this page