Skip to content
Primitiv home
Framework
Consumption mode

Popover

stableSource Figma

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

Density

Preview

Size
import { Popover, PopoverTrigger, PopoverContent, PopoverTitle, PopoverDescription, PopoverClose } from "@/components/ui/popover";
<Popover>  <PopoverTrigger asChild>    {/* wire a unique anchor-name ↔ position-anchor pair */}    <button style={{ anchorName: "--pop" }}>Open popover</button>  </PopoverTrigger>  <PopoverContent size="md" style={{ positionAnchor: "--pop" }}>    <PopoverTitle>Notifications</PopoverTitle>    <PopoverDescription>You have 3 unread messages.</PopoverDescription>    <PopoverClose asChild><button>Close</button></PopoverClose>  </PopoverContent></Popover>

Density is set by a data-density ancestor — the Context system, not a Popover prop.

Installation

npx 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>  <PopoverTrigger />  <PopoverContent>    <PopoverTitle />    <PopoverDescription />    <PopoverClose />  </PopoverContent></Popover>

Props

Popover.Root

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
childrenReactNodeheadlessThe Popover sub-components — typically a PopoverTriggerProps`Popover.Trigger` and a PopoverContentProps`Popover.Content`, optionally a PopoverAnchorProps`Popover.Anchor`.
defaultOpenbooleanfalseheadlessWhether 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) => voidheadlessCalled 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.
openbooleanheadlessForbidden 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.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender 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.
childrenReactNodeheadlessTrigger content (label, icon, etc.).
refRef<T>headlessRef 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.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the single child element instead of the default <div>, projecting the anchor onto an existing element via the Slot pattern.
childrenReactNodeheadlessAnchor content — the element(s) the popover should be positioned against.

Popover.Content

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender 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.
childrenReactNodeheadlessPanel content — typically PopoverTitleProps`Popover.Title`, body markup, and a PopoverCloseProps`Popover.Close`.
refRef<HTMLDivElement>headlessAllows 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"mdstyledPanel size; data-density scales the padding within each size.
placement"top" | "top-start" | "top-end" | "right" | "right-start" | "right-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end"bottomstyledWhich 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.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender 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.
childrenReactNodeheadlessTitle content, rendered as the popover's accessible name.

Popover.Description

Extends HTMLParagraphElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the single child element instead of the default <p> via the Slot pattern; the generated id is still applied and registered.
childrenReactNodeheadlessDescription content, exposed as the popover's accessible description.

Popover.Close

Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender 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.
childrenReactNodeheadlessClose-button content (label, icon, etc.).

Styling contract

--primitiv-popover-surface--primitiv-popover-fg--primitiv-popover-shadow--primitiv-popover-radius--primitiv-popover-padding-inline--primitiv-popover-padding-block--primitiv-popover-gap--primitiv-popover-arrow-size--primitiv-popover-offset--primitiv-popover-max-inline-size--primitiv-popover-title-color--primitiv-popover-title-font-family--primitiv-popover-title-font-size--primitiv-popover-title-font-weight--primitiv-popover-title-line-height--primitiv-popover-description-color--primitiv-popover-description-font-family--primitiv-popover-description-font-size--primitiv-popover-description-font-weight--primitiv-popover-description-line-height

Keyboard

KeyBehaviour
Enter / SpaceOpen the popover from the focused trigger.
EscapeClose the popover and return focus to the trigger.
TabMove through the panel's focusable content; leaving it does not force it closed (non-modal).

Data attributes

PopoverContent

className: .primitiv-popover

AttributeValueWhen
data-stateopen | closedopen / 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

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.

Density
import { Popover, PopoverTrigger, PopoverContent, PopoverTitle, PopoverDescription, PopoverClose } from "@/components/ui/popover";
<Popover>  <PopoverTrigger asChild>    <button style={{ anchorName: "--share" }}>Share</button>  </PopoverTrigger>  <PopoverContent style={{ positionAnchor: "--share" }}>    <PopoverTitle>Share this page</PopoverTitle>    <PopoverDescription>Anyone with the link can view it.</PopoverDescription>    <PopoverClose asChild><button>Done</button></PopoverClose>  </PopoverContent></Popover>

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.

Density
import { Popover, PopoverTrigger, PopoverContent, PopoverTitle, PopoverDescription, PopoverClose } from "@/components/ui/popover";
<PopoverContent placement="right" style={{ positionAnchor: "--pop" }}></PopoverContent>