where the popups inside a surface open, declared once: a fullscreen player's layer
npx shadcn add @ag/portalAlready installed? Preview an update
npx shadcn add @ag/portal --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-12
- New:
PortalTarget, where the popups inside a surface open, declared once.tooltip,popover,hover-card,menu,select,dialogandsheetread it as their defaultcontainer(one given its own still goes there), so a surface that shows on its own (a player in fullscreen, where a popup on the body is invisible) says it once instead of handing a container to each popup.usePortalTarget()reads it for a popup of your own. A setting's tip in a fullscreen player, which no one could hand a container, now shows.
also called portal container, popup container
shelf foundations
used by tooltip, dialog, menu, select, popover, hover-card, sheet, playback, film-mosaic
the demo above, verbatim
"use client"
import * as React from "react"
import { Sample } from "@/app/samples"
import { Button } from "@/registry/base-nova/ui/button"
import {
Popover,
PopoverContent,
PopoverTitle,
PopoverTrigger,
} from "@/registry/base-nova/ui/popover"
import { PortalTarget } from "@/registry/base-nova/ui/portal"
import { Tip } from "@/registry/base-nova/ui/tooltip"
/** A surface that goes fullscreen on its own: only it is drawn then, so a popup on
* the page's body would be invisible. It declares a layer inside itself once, and
* the tooltip and the popover in it open there with no container passed. */
function Stage() {
const stage = React.useRef<HTMLDivElement>(null)
const [layer, setLayer] = React.useState<HTMLElement | null>(null)
const full = () =>
document.fullscreenElement
? document.exitFullscreen()
: stage.current?.requestFullscreen()
return (
<div
ref={stage}
className="relative grid h-48 place-items-center rounded-xl bg-foreground/5"
>
<span
ref={setLayer}
className="contain-layout absolute top-0 left-0 z-50 size-0"
/>
<PortalTarget element={layer}>
<div className="flex gap-2">
<Tip content="opens inside the surface, fullscreen too">
<Button variant="secondary">a tip</Button>
</Tip>
<Popover>
<PopoverTrigger render={<Button variant="secondary" />}>
a popover
</PopoverTrigger>
<PopoverContent>
<PopoverTitle>inside the surface</PopoverTitle>
</PopoverContent>
</Popover>
<Button onClick={full}>fullscreen</Button>
</div>
</PortalTarget>
</div>
)
}
export default function PortalDemo() {
return (
<Sample
name="fullscreen"
glance="wide"
title="popups in a fullscreen surface"
description="go fullscreen: the tip and the popover still show, declared once"
>
<Stage />
</Sample>
)
}the source, every file an install writes
"use client"
import * as React from "react"
// WHERE A POPUP GOES. A popup (a tooltip, a popover, a menu, a select's list, a
// dialog) is portalled out of the flow, to the page's body by default. A surface that
// shows on its own declares where the popups inside it go instead, once: a player in
// fullscreen (only the fullscreen element is drawn, so a popup on the body is
// invisible), a frame that clips, a layer above a picture. Every popup item in the
// registry reads it as its default `container`; one given a `container` of its own
// still goes there.
//
// <PortalTarget element={layerElement}>
// … every Tip, Popover, Menu, Select and Dialog inside portals into it
// </PortalTarget>
//
// Surfaces nest: the nearest declaration wins, and a surface hands the popups on to
// the one around it by declaring that one (`usePortalTarget()`) instead of its own,
// as the player does while it is not fullscreen. No declaration, or an element not
// mounted yet, is the body: `usePortalTarget` says undefined then, never null,
// because Base UI's Portal reads a null container as one still to come and renders
// nothing (every popover outside a player stayed shut when it was null).
const PortalTargetContext = React.createContext<HTMLElement | null>(null)
/** Popups inside go into `element` (null or undefined: the page's body). */
export function PortalTarget({
element,
children,
}: {
element: HTMLElement | null | undefined
children: React.ReactNode
}) {
return (
<PortalTargetContext.Provider value={element ?? null}>
{children}
</PortalTargetContext.Provider>
)
}
/** Where a popup here goes: the nearest `PortalTarget`'s element, else undefined
* (the body). A popup item's default `container`. */
export function usePortalTarget(): HTMLElement | undefined {
return React.useContext(PortalTargetContext) ?? undefined
}