Skip to content
Component installs need the registry setup
VegaStack Design

Popover

An anchored, dismissible surface for secondary content — a trigger, a portaled popup with a header, title and description, and side/align positioning.

Status
stable
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 popover

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/popover

Run 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

Popover — data-slot="popover"
PopoverContent — data-slot="popover-content"
PopoverDescription — data-slot="popover-description"
PopoverHeader — data-slot="popover-header"
PopoverTitle — data-slot="popover-title"
PopoverTrigger — data-slot="popover-trigger"
PopoverAnchor — data-slot="popover-anchor"
PopoverBody — data-slot="popover-body"

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
        └── PopoverDescription

Basic

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

PropTypeDefaultDescription
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-expanded and aria-controls, the popup is a role="dialog" labelled by PopoverTitle and described by PopoverDescription, 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 PopoverContent re-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-visible outline from base.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.
ContractStates tested
Behaviourdefault, open, closed
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation
Visualdefault, open, closed

Do / Don't

Do
Use a Popover for secondary content the user can dismiss and ignore — a filter, a short form, a detail panel anchored to its trigger.
Don't
Use a Popover for a decision the user must resolve before continuing, or for a list of commands — that is a Dialog and a DropdownMenu respectively.

Deviations

Upstream's file plus packages/ui/upstream/patches/popover.patch. Every hunk:

  • OVL-13 — PopoverContent reads useInternalThemeScope() and attaches it to the Positioner INSIDE the portal — the same host shape select, combobox and tooltip carry. tooling/verify-portal-theme-scope.mjs inventories this host.
  • BRD-1 — PopoverContent draws a real 1px border border-border instead of upstream's ring-1 ring-foreground/10 box-shadow outline. A border survives forced colours and matches Elevation; nothing else about the surface moves.
  • LAY-17 — PopoverContent sets wrap-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 owns cn.
  • DOC-1 — prettier reflow plus the registry-stamp.mjs provenance header the three-copy model needs; no decision of its own.

On this page