Popover
A floating panel that appears next to its trigger and holds interactive content. Click-activated, with full keyboard support, positioning, an optional arrow, and compound parts for header, body, and footer.
pnpm add @tessinaui/uiWhen to use
- Interactive content next to a trigger. A form, a list or a few buttons that a user opens with a click or tap and works in without leaving the page.
- A small filter, share or rename panel. Header, body and footer parts give it structure, and the footer actions close it on click.
- A non-modal helper. By default clicks outside dismiss it while the page stays interactive; pass
modalfor a destructive confirmation that needs focus held inside. - A custom picker built from your own controls, anchored to the button that opens it.
- Supplementary detail the page can do without until asked for.
When not to use
- For a short, read-only hint on hover or focus. Use Tooltip.
- For a read-only preview that opens on hover. Use HoverCard.
- For a list of commands the user picks one from. Use DropdownMenu, which gives arrow-key navigation between items.
- When the decision must block the page, or the content is large and needs the user's full focus. Use Modal.
- For critical information or the page's main action. Content behind a click is easy to miss.
Installation
pnpm add @tessinaui/uiUsage
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverTitle,
PopoverDescription,
PopoverBody,
PopoverFooter,
PopoverArrow,
PopoverClose,
} from "@tessinaui/ui";<Popover size="md" rounded="md">
<PopoverTrigger render={<Button>Open</Button>} />
<PopoverContent side="bottom" align="center">
<PopoverHeader showClose>
<PopoverTitle>Popover title</PopoverTitle>
<PopoverDescription>Brief description.</PopoverDescription>
</PopoverHeader>
<PopoverBody>
<p>Any interactive content goes here — forms, lists, buttons…</p>
</PopoverBody>
<PopoverFooter>
<PopoverClose render={<Button variant="secondary">Cancel</Button>} />
<PopoverClose render={<Button>Confirm</Button>} />
</PopoverFooter>
<PopoverArrow />
</PopoverContent>
</Popover>Examples
Default
A basic popover with a header, description, body, and arrow.
Sizes
Four sizes — sm, md, lg, and xl — scale the width and text. Open each trigger to compare.
Intents
Semantic border colours — error, warning, success, and info.
Rounded
Corner radius of the popup surface, from none to full.
Curving your own content
Content you put inside — an image or swatch inside the panel — should curve like the chrome around it, or it reads as pasted on. The popover panel publishes its inner radius as a CSS variable:
<img className="rounded-[var(--popover-item-radius)]" />It tracks whatever the component is actually doing: the explicit step when rounded is set (6px at md), and 0 at rounded="none" — so your content squares off exactly when the container does.
Positioning
Place the popup on any side of the trigger. Each trigger opens its popup on the side it names — near a viewport edge the popup still flips to stay visible.
Confirmation
A compact destructive confirmation with footer actions that close on click.
Scrollable
A popover taller than the viewport clamps to the space the positioner measured; the header — with its pinned search and filter row — and the footer hold their edges, and only the body scrolls.
Filter panel
The filter-panel archetype: section labels, chip filters, a from/to range, and a footer that clears or applies — every control inside is the design system's own.
Skeleton
On-color
On a coloured Surface the trigger takes the plate's ink and its focus ring from context — no prop needed. The panel stays a standard popover: a portal floats it above the plate rather than on it, so its text and actions read exactly as they do on the page.
Anatomy
Popover composes several parts. Only Popover, PopoverTrigger, and PopoverContent are required — everything else is optional.
<Popover>
<PopoverTrigger>…</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle />
<PopoverDescription />
</PopoverHeader>
<PopoverBody />
<PopoverSeparator />
<PopoverFooter />
<PopoverArrow />
</PopoverContent>
</Popover>Size
| Size | Min width | Max width | Text |
|---|---|---|---|
"sm" | 200px | 240px | xs |
"md" (default) | 260px | 320px | sm |
"lg" | 320px | 400px | sm |
"xl" | 400px | 480px | base |
Intent
Applies a semantic border colour to the popup and arrow — useful for warning/error confirmations.
| Value | Border |
|---|---|
"none" (default) | border-border |
"error" | border-error |
"warning" | border-warning |
"success" | border-success |
"info" | border-info |
Rounded
| Value | CSS |
|---|---|
"none" | rounded-none |
"sm" | rounded-md |
"md" (default) | rounded-lg |
"lg" | rounded-xl |
"full" | rounded-3xl |
Positioning
PopoverContent accepts side (top | right | bottom | left), align (start | center | end), sideOffset (px gap from trigger, default 18; with an arrow the component floors it at 21 so the tip cannot park on the trigger), alignOffset (px shift along the side, default 0), and collisionPadding (px gap kept from the viewport edges before the popup flips or shifts, default 8).
On phones the popup additionally clamps itself: width never exceeds 100dvw - 2rem, and height never exceeds the space the positioner measured — a longer PopoverBody scrolls instead of running off-screen.
<PopoverContent side="right" align="start" sideOffset={12}>
…
</PopoverContent>Arrow
Opt in per content: pass arrow on PopoverContent for the built-in tail, or compose <PopoverArrow/> yourself to style it (the explicit child wins). The same arrow prop exists on HoverCardContent and DropdownMenuContent.
The <PopoverArrow /> is an SVG speech-bubble tail whose outline continues the popup's own border, inheriting the current intent's colour. Its apex radius and its clearance from the popup's corner both follow the rounded cascade. It's entirely optional — omit it for a chip-style floating panel. It is the house tail: HoverCard, DropdownMenu (so ContextMenu and Menubar), Tooltip, CoachMark and NavigationMenu all point with the same contour, drawn from one shared module — Tooltip at a smaller scale, because a tip is text rather than a panel.
Backdrop & modal behaviour
By default the popover is non-modal — clicks outside dismiss but the page remains interactive. For destructive confirmations or flows that need focus-trapping, pass modal:
<Popover modal>
…
</Popover>To dim the background, render <PopoverBackdrop /> inside the popover:
<Popover modal>
<PopoverTrigger render={<Button>Delete</Button>} />
<PopoverBackdrop />
<PopoverContent>…</PopoverContent>
</Popover>Controlled
Popover accepts open, defaultOpen, and onOpenChange.
const [open, setOpen] = React.useState(false);
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={<Button>Open</Button>} />
<PopoverContent>…</PopoverContent>
</Popover>Close button in header
PopoverHeader accepts showClose — renders a small X button wired to close the popover. Only use when the popover is long-lived or contains interactive content that makes the dismiss affordance non-obvious.
<PopoverHeader showClose>
<PopoverTitle>Rename file</PopoverTitle>
</PopoverHeader>API Reference
Popover (root)
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "md" | Min/max width + text size |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Corner radius of popup |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Semantic border colour |
dir | "ltr" | "rtl" | inherited | Reading direction. Unset, read off the trigger when the popup opens |
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Uncontrolled initial state |
onOpenChange | (open, event) => void | — | Fired when open state changes |
modal | boolean | false | Trap focus and block page interaction |
delay | number | — | Deprecated — has never had an effect. Base UI takes a hover delay on the Trigger (with openOnHover), not the root. Pass it there instead. |
PopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "bottom" | Which side of the trigger to position on |
align | "start" | "center" | "end" | "center" | Alignment along the side axis |
sideOffset | number | 18 | Distance in px from the trigger. With arrow, floored at 21 (13px tail + 8px air) |
arrow | boolean | false | Render the speech-bubble tail pointing at the trigger |
collisionAvoidance | object | — | Base UI collision config — pass { side: "none" } to keep a declared side instead of flipping at a viewport edge. Reference-only, same reason as container |
alignOffset | number | 0 | Shift along the side axis |
portal | boolean | true | Render inside a portal. false keeps the popup inside the component's own subtree, where it scales with a transformed ancestor — there is a Portal toggle in the playground above |
container | HTMLElement | null | document.body | Portal target. Reference-only — it exists for embedding a popover in a harness you control, so any example would picture the scaffolding rather than the popover |
collisionPadding | number | 8 | Minimum gap from the viewport edges |
positionerClassName | string | — | Extra classes on the positioner — raise its z-50 when the popover sits in a higher stacking context |
size, rounded, intent | — | inherited | Override the root's variant |
PopoverHeader
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Leading icon rendered beside the title |
trailing | ReactNode | — | Trailing slot on the title row — a badge, count, or status |
showClose | boolean | false | Render the built-in X close button |
PopoverTrigger, PopoverClose
Render a <button>. Use render={<CustomElement />} to swap in a Button or any element. Standard Base UI render-prop pattern.
Sub-components
PopoverTitle, PopoverDescription, PopoverBody, PopoverSeparator, PopoverFooter, PopoverBackdrop, PopoverArrow — all accept className and any HTML props. PopoverFooter runs the library's footer rhythm (pt-2, stepping up to pt-3 directly after a PopoverSeparator so actions never crowd the hairline). PopoverBody is the scroll region when content exceeds the available height. PopoverSkeleton renders the popup chrome with placeholder bars while content loads (size, rounded, intent, showTitle, lines, showFooter, footerActions).
Notes
- Built on
@base-ui/react/popover— the same headless primitive used by the design system for tooltips and menus. - Enter/exit animations use Base UI's
data-[starting-style]anddata-[ending-style]plus position-awaredata-[side=*]attributes, so the popup "lifts off" from whichever side it opens on. - The arrow is an SVG path drawn above the popup, so its fill covers the popup's border across the tail's base and its own outline carries on where that border stops — one continuous silhouette rather than a diamond stuck to an edge. It inherits the border colour automatically when
intentchanges.
Accessibility
VoiceOver + Safari: passed 2026-09-14. The trigger reads "Open popover, dialog pop up button"; Space opens "Popover title, web dialog" on its level-2 heading, and Tab with nothing focusable inside returns to the trigger. NVDA and JAWS are untested.
- The trigger and popup are linked via ARIA — screen readers announce the popup's content when it opens.
PopoverTitlemaps toaria-labelledby,PopoverDescriptiontoaria-describedby.- Focus moves to the first focusable element in the popup on open, and returns to the trigger on close.
Escapecloses the popover, and so does a click outside it. To keep it open through both, controlopenyourself and ignore the dismissal inonOpenChange— the callback'sreasontells you which one fired.- Use
intent="error"for destructive confirmations so assistive tech surfaces the semantic colour via the rendered border.
RTL
Pass dir="rtl" to the root. PopoverHeader's close button, PopoverFooter actions, and align all follow the reading direction, so align="start" lines the panel up with the trigger's right edge. Leave dir unset and the popup follows the direction around the trigger that opened it, including an RTL wrapper that is not <html>.