Popover
A click-triggered floating panel anchored to its trigger, for rich, interactive
content — a share box, a filter form, quick details. Built on Base UI
Popover. Unlike a Tooltip, it can hold focusable controls; unlike a
Dialog, it stays anchored and doesn’t trap the user.
Import
import { Popover } from "@photon-ai/pho-ui/components/popover";
Basic
Popover.Trigger opens the panel; it closes on click-away or Escape. Compose
Title / Description and any controls inside Popover.Popup. The render
prop lets the trigger and close reuse a Button.
<Popover.Root>
<Popover.Trigger render={<Button variant="outlined">Share</Button>} />
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Title>Share project</Popover.Title>
<Popover.Description>
Anyone with the link can view.
</Popover.Description>
<Popover.Close render={<Button size="sm">Copy link</Button>} />
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
Placement
Popover.Positioner controls where the panel sits: side picks the edge
(bottom by default), align sets alignment along it (center by default),
and sideOffset is the gap — defaulted to 8 here. Placement flips
automatically when the chosen side would overflow the viewport.
<Popover.Positioner side="right" align="start">
<Popover.Popup>…</Popover.Popup>
</Popover.Positioner>
Anatomy
- Popover.Root — owns open state (
open/defaultOpen,modal) - Popover.Trigger — the button that toggles the panel
- Popover.Portal — renders the panel at the document root, above other layers
- Popover.Positioner — anchors the panel (
side,align,sideOffset) - Popover.Popup — the floating panel; holds the content
- Popover.Title — names the panel; wires
aria-labelledby - Popover.Description — supporting text; wires
aria-describedby - Popover.Close — dismisses the panel from inside
Props
Popover.Root
- open / defaultOpen —
boolean— controlled / uncontrolled open state - onOpenChange —
(open, eventDetails) => void - modal —
false·true·trap-focus(defaultfalse) —truelocks page scroll and disables outside interaction - onOpenChangeComplete —
(open: boolean) => void— fires after the open/close animation settles
Popover.Trigger (renders a <button>)
- render — element to render as the trigger (e.g. a
Button) - openOnHover —
boolean(defaultfalse) — also open on hover - delay / closeDelay —
number(default300/0) — hover open / close delay in ms
Popover.Positioner (renders a <div>)
- side —
top·right·bottom·left(defaultbottom) - align —
start·center·end(defaultcenter) - sideOffset —
number(default8) — gap from the anchor in px - alignOffset —
number(default0) — offset along the alignment axis in px - collisionPadding —
number(default5) — space kept from the viewport edge - sticky —
boolean(defaultfalse) — keep the panel visible after the anchor scrolls off
Popover.Popup (renders a <div role="dialog">)
- initialFocus — element focused on open (default: first tabbable inside)
- finalFocus — element focused on close (default: the trigger)
Accessibility
The popup renders as role="dialog". Popover.Title supplies its accessible
name via aria-labelledby and Popover.Description its aria-describedby, so
including a Title is what makes the panel announce its purpose on open —
otherwise give the Popup an aria-label. The trigger is wired with
aria-haspopup="dialog", aria-expanded, and aria-controls.
- Enter / Space — toggle the popover from the trigger
- Escape — close and return focus to the trigger
- Tab / Shift+Tab — move through the focusable controls inside the panel
- Click outside — dismiss the panel
On open, focus moves to the first tabbable element inside the popup (or the
popup itself when opened by touch); on close it returns to the trigger. Focus is
not trapped by default — set modal to true or 'trap-focus' to trap it, and
render a Popover.Close inside the popup so touch screen-reader users can exit.
Best practices
- Use a popover for content that’s interactive but secondary — it shouldn’t
trap the user the way a modal does. For a decision use
Dialog. - Anchor it to the control that opened it and keep it compact; long content
belongs on the page or in a
Dialog. - Give it a
Titleso its purpose is announced when it opens, and aCloseinside the panel when you setmodal. - For a hover-only affordance that holds no focusable controls, reach for
TooltiporPreviewCardinstead.