A non-modal floating panel anchored to a trigger, built on the native HTML Popover API — a borderless, shadow-defined surface with a pointer arrow, positioned with CSS anchor positioning across all twelve placements.
Playground
Preview
Notifications
You have 3 unread messages.
import{Popover,PopoverTrigger,PopoverContent,PopoverTitle,PopoverDescription,PopoverClose}from"@/components/ui/popover";<Popover><PopoverTriggerasChild>{/* wire a unique anchor-name ↔ position-anchor pair */}<buttonstyle={{ anchorName:"--pop"}}>Open popover</button></PopoverTrigger><PopoverContentsize="md"style={{ positionAnchor:"--pop"}}><PopoverTitle>Notifications</PopoverTitle><PopoverDescription>You have 3 unread messages.</PopoverDescription><PopoverCloseasChild><button>Close</button></PopoverClose></PopoverContent></Popover>
import{Popover}from"@primitiv-ui/react";<Popover.Root><Popover.TriggerasChild>{/* wire a unique anchor-name ↔ position-anchor pair */}<buttonstyle={{ anchorName:"--pop"}}>Open popover</button></Popover.Trigger><Popover.Contentstyle={{ positionAnchor:"--pop"}}><Popover.Title>Notifications</Popover.Title><Popover.Description>You have 3 unread messages.</Popover.Description><Popover.CloseasChild><button>Close</button></Popover.Close></Popover.Content></Popover.Root>
Density is set by a data-density ancestor — the Context system, not a Popover prop.
Installation
npx primitiv add popover
pnpm dlx primitiv add popover
yarn dlx primitiv add popover
bunx primitiv add popover
Import
import{Popover}from"@/components/ui/popover";
Copied into your project as .primitiv-popover — you own the file afterwards, so upgrades are opt-in.
Headless mode installs the npm package instead: @primitiv-ui/react
Anatomy
Popover.Root owns the open state; Popover.Trigger is the button that toggles it; Popover.Content is the panel, holding an optional Popover.Title + Popover.Description (wired to the panel as its accessible name/description) and a Popover.Close. Positioning is CSS anchor positioning, which you wire — a unique anchor-name on the trigger, a matching position-anchor on the content (Popover.Anchor lets you anchor to a different element than the trigger).
Generated from source — headless rows from each *Props type’s JSDoc, styled rows from contract.json. Never hand-maintained.
Popover.Root
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
children
ReactNode
—
headless
The Popover sub-components — typically a PopoverTriggerProps`Popover.Trigger`
and a PopoverContentProps`Popover.Content`, optionally a
PopoverAnchorProps`Popover.Anchor`.
defaultOpen
boolean
false
headless
Whether the popover is open on first render. The component owns the flag
from then on (trigger toggles, Popover.Close, Escape, and light-dismiss
all mutate it internally).
Forbidden in controlled mode — use open instead.
onOpenChange
(open: boolean) => void
—
headless
Called with the new open state after every transition. Optional in
uncontrolled mode; use it to observe (not drive) open/close.
Called with the requested open state whenever the popover wants to open
or close (trigger click, Popover.Close, Escape, outside dismiss).
Required in controlled mode — the popover will not move unless the parent
updates open.
open
boolean
—
headless
Forbidden in uncontrolled mode — use defaultOpen instead.
The current open state. Must be kept in sync by the parent via
onOpenChange.
Popover.Trigger
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the single child element instead of the default <button>,
merging the trigger's ARIA, composed event handlers, and ref onto it via
the Slot pattern.
children
ReactNode
—
headless
Trigger content (label, icon, etc.).
ref
Ref<T>
—
headless
Ref to the rendered element, composed with Root's internal trigger ref
(used for focus return on close). Defaults to HTMLButtonElement; with
asChild, set the generic to the child's element type
(e.g. HTMLAnchorElement).
Popover.Anchor
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the single child element instead of the default <div>, projecting
the anchor onto an existing element via the Slot pattern.
children
ReactNode
—
headless
Anchor content — the element(s) the popover should be positioned against.
Popover.Content
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the single child element (e.g. a <section> or motion wrapper)
instead of the default <div>, merging the dialog props and internal ref
onto it via the Slot pattern.
children
ReactNode
—
headless
Panel content — typically PopoverTitleProps`Popover.Title`,
body markup, and a PopoverCloseProps`Popover.Close`.
ref
Ref<HTMLDivElement>
—
headless
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Ref to the rendered element, composed with Root's internal content ref so
open/close and focus management keep working.
size
"sm" | "md" | "lg" | "xl"
md
styled
Panel size; data-density scales the padding within each size.
Which side of the anchor the panel sits on, and how it aligns — sets the CSS position-area and points the arrow at the anchor. Requires the consumer to wire anchor-name (on the trigger or Popover.Anchor) + position-anchor (on Content).
Popover.Title
Extends HTMLHeadingElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the single child element (e.g. an <h3>) instead of the default
<h2> via the Slot pattern; the generated id is still applied
and registered.
children
ReactNode
—
headless
Title content, rendered as the popover's accessible name.
Popover.Description
Extends HTMLParagraphElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the single child element instead of the default <p> via the
Slot pattern; the generated id is still applied and registered.
children
ReactNode
—
headless
Description content, exposed as the popover's accessible description.
Popover.Close
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the single child element (e.g. a text link or icon button) instead
of the default <button>, merging the close behaviour onto it via the
Slot pattern.
children
ReactNode
—
headless
Close-button content (label, icon, etc.).
Styling contract
20 CSS custom properties on .primitiv-popover — mode-agnostic. These names are the stable surface; the values are not.
The trigger is a button, so Space/Enter open it. Once open, focus moves into the panel; Escape closes it and returns focus to the trigger. It is non-modal — Tab can leave the panel — and clicking outside light-dismisses it.
Key
Behaviour
Enter / Space
Open the popover from the focused trigger.
Escape
Close the popover and return focus to the trigger.
Tab
Move through the panel's focusable content; leaving it does not force it closed (non-modal).
Data attributes
Emitted automatically by the headless primitive — style against these rather than adding your own state classes. Grouped by the part that emits them, since most are emitted by more than one.
PopoverContent
className:.primitiv-popover
Attribute
Value
When
data-state
open | closed
open / closed
Popover.Content
Attribute
Value
When
data-state
open | closed
open / closed
Accessibility
Popover.Content is a native [popover] in the top layer, so it always paints above the rest of the page regardless of z-index or overflow: hidden ancestors — no stacking-context surprises.
Popover.Title and Popover.Description are wired to the panel as its aria-labelledby/aria-describedby, so a screen reader announces what the popover is on open rather than reading a bare panel.
It is non-modal: it does not trap focus, and Tab can move out of the panel back into the page. Escape closes it and returns focus to the trigger; clicking outside light-dismisses it.
Focus moves into the panel on open and returns to the trigger on close, so a keyboard user is taken to the content and brought back — never stranded where the panel used to be.
Reach for Modal instead when the task must be completed or dismissed before anything else — a popover is for optional, in-context content, not a blocking decision.
Examples
Every example below reacts to the density control. Currently showing Styled mode.
A titled panel
Click the trigger to open a small panel of rich content — a form, a set of details, a menu of links. Popover.Title and Popover.Description become the panel's accessible name and description, and Popover.Close dismisses it. Click outside or press Escape to close. The anchor pair (a unique anchor-name on the trigger, matching position-anchor on the content) is what places it.
Share this page
Anyone with the link can view it.
import{Popover,PopoverTrigger,PopoverContent,PopoverTitle,PopoverDescription,PopoverClose}from"@/components/ui/popover";<Popover><PopoverTriggerasChild><buttonstyle={{ anchorName:"--share"}}>Share</button></PopoverTrigger><PopoverContentstyle={{ positionAnchor:"--share"}}><PopoverTitle>Share this page</PopoverTitle><PopoverDescription>Anyone with the link can view it.</PopoverDescription><PopoverCloseasChild><button>Done</button></PopoverClose></PopoverContent></Popover>
import{Popover}from"@primitiv-ui/react";<Popover.Root><Popover.TriggerasChild><buttonstyle={{ anchorName:"--share"}}>Share</button></Popover.Trigger><Popover.Contentstyle={{ positionAnchor:"--share"}}><Popover.Title>Share this page</Popover.Title><Popover.Description>Anyone with the link can view it.</Popover.Description><Popover.CloseasChild><button>Done</button></Popover.Close></Popover.Content></Popover.Root>
Placement
placement sets where the panel opens relative to the trigger — the same 13 values as Tooltip (top/right/bottom/left × -start/-end). It flips to the opposite side when there is not room, so a bottom popover near the viewport floor opens upward instead. Click each trigger to see it.