Photon Design System

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 (default false) — true locks 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 (default false) — also open on hover
  • delay / closeDelay — number (default 300 / 0) — hover open / close delay in ms

Popover.Positioner (renders a <div>)

  • side — top · right · bottom · left (default bottom)
  • align — start · center · end (default center)
  • sideOffset — number (default 8) — gap from the anchor in px
  • alignOffset — number (default 0) — offset along the alignment axis in px
  • collisionPadding — number (default 5) — space kept from the viewport edge
  • sticky — boolean (default false) — 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 Title so its purpose is announced when it opens, and a Close inside the panel when you set modal.
  • For a hover-only affordance that holds no focusable controls, reach for Tooltip or PreviewCard instead.