Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Popover

A click-triggered floating panel for arbitrary content — positioning, an optional arrow, and built-in dismiss.

Status
stable
Since
0.1.0
Accessibility pattern
APG dialog (non-modal)

Last updated

Install

Add Popover from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/popover

The same command installs the registry items it composes: @vegastack/floating-surface.

Usage

import {
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverClose,
  PopoverTitle,
  PopoverDescription,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";

<Popover>
  <PopoverTrigger render={<Button variant="outline">Open popover</Button>} />
  <PopoverContent>
    <PopoverTitle>About this layer</PopoverTitle>
    <PopoverDescription>
      Floats arbitrary content next to the trigger.
    </PopoverDescription>
  </PopoverContent>
</Popover>;

Anatomy

Popover is a compound component built on Base UI's Popover. Compose the parts inside the root:

Popover
PopoverArrow
PopoverClose — data-slot="popover-close"
PopoverContent
PopoverDescription — data-slot="popover-description"
PopoverTitle — data-slot="popover-title"
PopoverTrigger — data-slot="popover-trigger"
<Popover>
  <PopoverTrigger render={<Button>Open</Button>} />
  <PopoverContent side="bottom" align="center">
    <PopoverTitle>Title</PopoverTitle>
    <PopoverDescription>Supporting description text.</PopoverDescription>
    {/* arbitrary content — text, a form, a menu, ... */}
    <PopoverClose render={<Button variant="outline">Done</Button>} />
    {/* <PopoverArrow /> — or pass `arrow` to PopoverContent */}
  </PopoverContent>
</Popover>
  • Popover — the root; owns open/close state (open / defaultOpen / onOpenChange). Modal by default in VegaStack so anchored pickers do not shift while the page scrolls; pass modal={false} for a lightweight non-blocking popover.
  • PopoverTrigger — the control that opens the popover on click (data-slot="popover-trigger"). Pass render to compose it with a Button.
  • PopoverContent — the floating panel (data-slot="popover-content"). Composes Base UI's Portal + Positioner + Popup, owns side / sideOffset / align / collisionPadding, exposes portalProps / positionerProps, can wrap children in an optional Base UI Viewport via viewportProps, animates enter/exit, and optionally renders an arrow.
  • PopoverTitle — the popover's accessible name (data-slot="popover-title", <h2>). Wired to the popup via aria-labelledby.
  • PopoverDescription — supporting text (data-slot="popover-description", <p>). Wired via aria-describedby.
  • PopoverClose — closes the popover (data-slot="popover-close"). Pass render to compose it with a Button.
  • PopoverArrow — a triangle anchored to the trigger (data-slot="popover-arrow"). Rendered automatically when PopoverContent gets arrow, or composed directly.

Examples

Form panel

Popovers can host arbitrary interactive content — here a small inline form. Use PopoverClose for the action buttons so they dismiss the panel.

Sides

side places the panel on the top, right, bottom, or left of the trigger (default bottom). Base UI flips the side automatically when it would collide with the viewport edge.

Alignment

align shifts the panel along the chosen side — start, center (default), or end — to line its edge up with the trigger.

Arrow

Pass arrow to PopoverContent to render a directional PopoverArrow pointing back at the trigger. You can also compose PopoverArrow directly inside the content for full control.

<PopoverContent>
  <PopoverArrow />
  <PopoverTitle>Direct compose</PopoverTitle>
  {/* ... */}
</PopoverContent>

Non-modal

By default a popover is modal — it locks background scroll so anchored pickers don't shift. Pass modal={false} for a lightweight, non-blocking popover that keeps the rest of the page scrollable and interactive.

Playground

Pick a side and toggle the arrow, open the popover, then copy the generated JSX.

<Popover>
  <PopoverTrigger render={<Button variant="outline">Open popover</Button>} />
  <PopoverContent>
    <PopoverTitle>About this layer</PopoverTitle>
    <PopoverDescription>Floats arbitrary content next to the trigger.</PopoverDescription>
  </PopoverContent>
</Popover>

API Reference

Popover

Popover adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

PopoverTrigger

PopoverTrigger adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Data attributes and CSS variables on PopoverTrigger

AttributeValues
data-slot"popover-trigger"

PopoverContent

PropTypeDefaultDescription
alignAlign"center"Alignment relative to the chosen side.
arrowbooleanfalseRender a directional arrow pointing at the trigger.
collisionPaddingPadding8Minimum distance to keep between the popover and the viewport edge, in pixels.
portalPropsOmit<Omit<PopoverPortalProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children">Props forwarded to the underlying Base UI Portal.
positionerPropsOmit<Omit<PopoverPositionerProps, "ref"> & React.RefAttributes<HTMLDivElement>, "align" | "children" | "collisionPadding" | "side" | "sideOffset">Props forwarded to the underlying Base UI Positioner.
sideSide"bottom"Which side of the trigger to place the popover on.
sideOffsetnumber | OffsetFunction8Distance between the trigger and the popover, in pixels.
viewportPropsOmit<Omit<PopoverViewportProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children">Props forwarded to an optional Base UI Viewport that wraps popup children.

PopoverClose

PopoverClose adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Data attributes and CSS variables on PopoverClose

AttributeValues
data-slot"popover-close"

PopoverArrow

PopoverArrow adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

PopoverTitle

PopoverTitle adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Data attributes and CSS variables on PopoverTitle

AttributeValues
data-slot"popover-title"

PopoverDescription

PopoverDescription adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Data attributes and CSS variables on PopoverDescription

AttributeValues
data-slot"popover-description"

Accessibility

  • When a PopoverTitle / PopoverDescription are present, the popup is wired to them as its aria-labelledby / aria-describedby — include a title whenever the panel has a heading.
  • The popover opens on trigger click. Focus moves into the panel (first tabbable element) on open and is restored to the trigger on close.
  • Dismiss is built in — clicking outside the panel or pressing Esc closes it.
  • The popup itself can receive keyboard focus (initial focus, focus wrap), so its native outline is deliberately not stripped — the centralized :focus-visible outline is its indicator (WCAG 2.4.7, register P0-02). Composed controls — the Button trigger, any Input inside, and the PopoverClose buttons — each carry their own visible focus treatment.
KeyAction
Enter / SpaceActivate the focused trigger to open the popover.
Tab / Shift + TabMove focus between controls inside the open panel.
EscClose the popover and return focus to the trigger.
ContractStates tested
Behaviourdefault, open
Accessibilitydescribed, focus-visible, labeled, semantic-html
Visualdefault

Do / Don't

Do
Use a Popover for lightweight, optional content tied to a trigger — a settings form, a color picker, extra detail. Include a PopoverTitle when the panel has a heading.
Don't
Use a Popover for content that must block the rest of the page or demands a decision — use a Dialog instead. And don't put a tooltip's worth of plain text in one — use a Tooltip.

On this page