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.
Playground
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.
When to use Popover vs. Tooltip vs. DropdownMenu vs. Modal
| Use Popover when… | Use Tooltip when… | Use DropdownMenu when… | Use Modal when… |
|---|---|---|---|
| You need interactive content | You just need informational text on hover | The content is a list of menu items | You need to block the rest of the page |
| The trigger is click-activated | The trigger is hover/focus | Users pick one option then dismiss | The decision is critical or blocking |
| The panel can contain a form, list, or buttons | The tip is short and non-interactive | Keyboard arrow navigation between items | Content is large or requires focus |
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.
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>Accessibility
- 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 alignment all flip to follow the reading direction.
<Popover dir="rtl">
<PopoverTrigger render={<Button>فتح</Button>} />
<PopoverContent side="bottom" align="start">…</PopoverContent>
</Popover>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" | "ltr" | Reading direction |
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.