Popover
An anchored, dismissible surface for secondary content — a trigger, a portaled popup with a header, title and description, and side/align positioning.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (non-modal)
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Popover before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify popoverInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/popoverRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
Open Popover
</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description text here.</PopoverDescription>
</PopoverHeader>
</PopoverContent>
</Popover>;One surface. A popover is one surface: no filled rows, no bg-muted tracks, no card inside
it. A chosen option is a check or a pressed toggle; rows are 32px tall from the popover's inset, on
an 8px rhythm. The list toolbar's popovers (ListToolbarPopover) take p-2 gap-2 and become a
bottom sheet on a phone.
Anatomy
Examples
Composition
PopoverContent composes the portal, the positioner and the popup, so a popover is two parts at the
call site.
Popover
├── PopoverTrigger
└── PopoverContent
└── PopoverHeader
├── PopoverTitle
└── PopoverDescriptionBasic
A header, a title and a description — the smallest popover that still names itself. PopoverTitle
labels the popup, so a screen reader announces the surface rather than its first sentence.
Align
align on PopoverContent takes start, center (the default) and end, and decides where the
popup sits along the trigger's edge. alignOffset, side and sideOffset are forwarded to the same
positioner.
With Form
The popup is a focus-scoped surface, so form controls inside it are reachable and submit normally.
Give each Input an id that its FieldLabel points at — the popup portals out of the trigger's
subtree, and an implicit label would not survive the move.
RTL
Wrap the subtree in DirectionProvider direction="rtl" so Base UI resolves the logical sides.
side="inline-start" and side="inline-end" swap with the reading direction; left, right, top
and bottom stay where they are named.
Playground
Move the surface around its anchor — side picks the edge, align the offset along it — then copy
the generated JSX.
<Popover>
<PopoverTrigger render={<Button variant="outline">Open popover</Button>} />
<PopoverContent>
<PopoverHeader>
<PopoverTitle>About this layer</PopoverTitle>
<PopoverDescription>Floats arbitrary content next to the trigger.</PopoverDescription>
</PopoverHeader>
</PopoverContent>
</Popover>Sizes, long content and an anchor
size sets the width: default (288px), md (320), lg (384, never wider than the window less
2rem) or fit (the content's own, up to 320). PopoverBody holds content that may run long — it
scrolls inside the popover from 70% of the window's height. PopoverDescription keeps line breaks
in its text and inks a strong in the foreground. For a popover opened from elsewhere (a menu
item), put a PopoverAnchor — an invisible 0×0 trigger — where it should point.
<Popover open={open} onOpenChange={setOpen}>
<PopoverAnchor />
<PopoverContent size="fit" align="end">
<PopoverDescription>
<strong>12</strong> words selected
</PopoverDescription>
</PopoverContent>
</Popover>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "fit" | "lg" | "md" | "default" | The popover's width: default 288px, md 320px, lg 384px (never wider than the window
less 2rem), fit its content's own width up to 320px. |
Popover, PopoverTrigger, PopoverHeader, PopoverTitle and PopoverDescription add no props of
their own — each accepts everything the Base UI part accepts.
Accessibility
- Renders Base UI's Popover: the trigger is a button with
aria-expandedandaria-controls, the popup is arole="dialog"labelled byPopoverTitleand described byPopoverDescription, and Esc closes it and returns focus to the trigger. - The popover is non-modal: the page behind it stays scrollable and interactive, and an outside press dismisses it.
- The popup portals out of the trigger's subtree, so
PopoverContentre-applies the theme scope on the positioner inside the portal (OVL-13) — a dark or scoped surface survives the portal. - Focus is the global 2px
:focus-visibleoutline frombase.css; there is no ring glow on the trigger or on anything inside the popup. - Give the trigger an accessible name. When it is an icon button, that means an
aria-label.
| Contract | States tested |
|---|---|
| Behaviour | default, open, closed |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation |
| Visual | default, open, closed |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/popover.patch. Every hunk:
- OVL-13 —
PopoverContentreadsuseInternalThemeScope()and attaches it to the Positioner INSIDE the portal — the same host shapeselect,comboboxandtooltipcarry.tooling/verify-portal-theme-scope.mjsinventories this host. - BRD-1 —
PopoverContentdraws a real 1pxborder border-borderinstead of upstream'sring-1 ring-foreground/10box-shadow outline. A border survives forced colours and matches Elevation; nothing else about the surface moves. - LAY-17 —
PopoverContentsetswrap-break-word, so a long unbroken word breaks inside the popover rather than running past its edge. MK, 2026-10-13. - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header the three-copy model needs; no decision of its own.