Component · Containers & Overlays
Popover
Overview
Triggered, non-modal supplemental content anchored to a control — richer than a Tooltip (can hold interactive content: buttons, links, a small form), but unlike Dialog it never blocks or dims the rest of the page. Its behavior — focus placement on open, collision-aware positioning, and close-on-outside-interaction — is specified below.
Anatomy
Trigger (any control) → Anchor relationship → Popover surface (Header/title optional, Body content, optional Footer/actions).
- Trigger — any control that opens the popover.
- Surface — the floating container anchored to the trigger.
- Header — optional title.
- Body — free-form content.
- Footer — optional actions.
Popover has no fixed internal anatomy beyond the surface itself — unlike Dialog, it doesn't mandate a Title/Description/Actions split; content is free-form, sized to what the instance needs.
Live Example
Informational — a data readout
Interactive — a small settings form
Code Example
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
/**
* Live Popover example — anchored, non-modal supplemental content. The page
* behind stays fully operable: no scrim, no scroll lock. `Escape` closes and
* returns focus to the trigger; a click outside closes it and leaves focus
* where the click landed; clicking the trigger again toggles it.
*
* - Informational: no focusable content, so focus stays on the trigger when it
* opens. It is still a Popover (toggled by a trigger, more structure than a
* Tooltip), not a Tooltip.
* - Interactive: contains fields and a button, so opening it from the keyboard
* moves focus to the first field. It carries an accessible name via
* `<PopoverTitle>` (wired through `aria-labelledby`).
*/
export function PopoverShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Informational — a data readout
</h3>
<div className="flex items-center gap-2 text-sm leading-normal">
<span>Storage used</span>
<Popover>
<PopoverTrigger>
<Button size="sm" variant="outline">
Details
</Button>
</PopoverTrigger>
<PopoverContent aria-label="Storage breakdown">
<dl className="grid grid-cols-[1fr_auto] gap-x-6 gap-y-1">
<dt className="text-muted-foreground">Documents</dt>
<dd>4.2 GB</dd>
<dt className="text-muted-foreground">Images</dt>
<dd>11.8 GB</dd>
<dt className="text-muted-foreground">Backups</dt>
<dd>2.1 GB</dd>
</dl>
</PopoverContent>
</Popover>
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Interactive — a small settings form
</h3>
<Popover>
<PopoverTrigger>
<Button variant="outline">Edit dimensions</Button>
</PopoverTrigger>
<PopoverContent className="w-64 max-w-none">
<PopoverTitle>Dimensions</PopoverTitle>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1.5">
<Label htmlFor="popover-width">Width</Label>
<Input id="popover-width" size="sm" defaultValue="1920" />
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="popover-height">Height</Label>
<Input id="popover-height" size="sm" defaultValue="1080" />
</div>
<div className="flex justify-end gap-2">
<PopoverClose>
<Button size="sm" variant="ghost">
Cancel
</Button>
</PopoverClose>
<PopoverClose>
<Button size="sm">Apply</Button>
</PopoverClose>
</div>
</div>
</PopoverContent>
</Popover>
</section>
</div>
);
}Variants
Content modes: informational vs. interactive
A Popover's content is always exactly one of two modes, and the mode — not a per-instance judgment call — determines focus behavior on open. Any link, button, form control, or other interactive element inside the Popover makes it Interactive; there is no ambiguous middle case (e.g. "a details panel that happens to contain one link").
Informational Popover
- Contains no links, buttons, form controls, or other interactive elements — plain content only (text, an image, a data readout).
- Focus stays on the trigger when the Popover opens.
- The Popover is programmatically associated with the trigger (
aria-expanded/aria-controls, oraria-describedbywhere that fits the content better) without stealing focus. - It's still a Popover, not reclassified as a Tooltip merely because it's non-interactive — its visibility is explicitly toggled by a trigger action, and it may hold more structure (a title, multiple lines, an image) than a Tooltip's plain text line allows.
Interactive Popover
- Any Popover containing a link, button, form field, or other control is Interactive — no exception for "just one link."
- It must have an accessible name: a visible title inside the Popover referenced via
aria-labelledby, or an equivalent programmatic label (e.g.aria-labelon the surface) when no visible title is used. - Opening by keyboard moves focus to the first meaningful interactive element inside the Popover, or to an appropriately focusable content container when no single element is the obvious first stop.
- Focus is not trapped — see Interactive content.
Escapecloses it and restores focus to the trigger.- Outside interaction may dismiss it.
States
| State | Look |
|---|---|
| Closed | Not rendered |
| Open | --popover fill, --popover-foreground text, --shadow-md, --border edge |
Usage Guidance
Purpose and when to use it
Use Popover for content that's genuinely optional to the current task, triggered on demand, and doesn't need to interrupt the user — a details panel for a data point, a small settings form attached to a control, a share panel. The page behind it stays fully operable.
When not to use it
- Passive, non-interactive labeling — that's Tooltip.
- Something that must block the current flow until resolved (a confirmation, a required decision) — that's Dialog.
- A list of navigable options/commands (an actions menu, a select-style list) — that's a
dropdown menu pattern (
role="menu"/role="listbox", roving arrow-key selection), which is a distinct component (Dropdown Menu / Select), not a Popover. Reaching for Popover to build a menu skips that pattern's real keyboard and semantic requirements.
Anchor relationship
The Popover is positioned relative to its trigger (anchor), not the viewport — it moves with its trigger and closes if the trigger leaves the viewport (e.g. scrolled out of view in a scrollable container).
Dismissal expectations
| Trigger | Behavior |
|---|---|
Escape | Always closes the popover and returns focus to the trigger |
| Click outside (outside both trigger and popover) | Closes the popover; focus stays wherever the click landed (not forced back to the trigger — the user deliberately moved elsewhere) |
| Trigger clicked again | Toggles closed |
| Focus moves within the trigger–popover relationship (e.g. trigger → its own popover content) | Never dismisses the popover |
Focus leaves both the trigger and the popover (e.g. Tab out to the next page element) | May dismiss the popover, when that matches the instance's interaction model — an Informational popover with no focusable content has nothing to Tab out of in the first place |
Interactive content
A Popover may contain buttons, links, inputs, or a small form. Because it's non-modal, focus is
not trapped inside it the way Dialog's is — a user can
Tab out of the popover into the rest of the page, which typically (implementation-dependent)
closes it, since it's no longer the relevant context.
Positioning and collision handling
Default placement is anchored to one side of the trigger (commonly below, left-aligned to the trigger's edge); on viewport collision, it should flip to the opposite side before shifting along the cross-axis — the same collision-avoidance intent Tooltip uses at smaller scale. Measurement and repositioning happen at render time against the real viewport. Collision handling is against the viewport only — it doesn't account for every scrollable ancestor — so a Popover inside a nested scroll container may need an explicit side.
Tokens
Popover uses existing Monogem semantic tokens to stay consistent with the design system; it introduces no component-specific styling values.
| Token | Where used |
|---|---|
--popover / --popover-foreground | Surface fill + text — Color names "Menus, tooltips" for this pair; Popover shares it |
--border | Surface edge |
--shadow-md | Elevation — Shadows names "Dropdowns, popovers, hover" |
--radius-lg | Corner radius — Radius explicitly names "Cards, popovers" for this token |
--ring | Focus ring on any focusable content inside the popover |
--scale-4 | Surface padding |
Do / Don’t
Do
- Keep Popover content focused on one task or one piece of information.
- Return focus to the trigger on
Escape. - Close the popover when its trigger scrolls out of view.
Don’t
- Use Popover to build a command/options menu — use a real menu pattern instead.
- Trap focus inside a Popover — that's Dialog's contract, not Popover's.
- Add a full-page backdrop/scrim behind a Popover — that visual weight belongs to Dialog.
Accessibility
- Trigger sets
aria-expanded(true/false) andaria-controlspointing at the popover'sid. - The popover surface itself is
role="dialog"witharia-modal="false"(or an equivalent non-modal container role) — non-modal, so background content remains in the accessibility tree and operable, unlike Dialog's modal contract. - Every interactive element inside the popover follows its own component's accessibility contract (a Button inside a Popover is still a Button) — Popover adds no new interaction pattern of its own.
- An Interactive Popover (see Content modes) carries
an accessible name — a visible title referenced by
aria-labelledby, or anaria-labelon the surface — required once it contains any focusable control, not optional.
Responsive Behavior
An anchored popover can be awkward on a narrow viewport — too little room to position it sensibly
next to a small trigger. Below --breakpoint-sm, an implementation may fall back to a
bottom-anchored presentation closer to Sheet's Bottom placement. This is a
documented intent, not a fully specified fallback component.
Related Components / Patterns
- Tooltip — a short label on hover or keyboard focus.
- Dialog — modal interruption until the user resolves it.
- Dropdown Menu — a menu of commands or actions.