DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
PopoverHoverCardDropdown MenuContextMenuModalAlertDialogDrawerAction Sheet
AI agentsContributing
  1. Components
  2. Popover

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/ui

When 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 modal for 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

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import {
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverDescription,
  PopoverBody,
  PopoverFooter,
  PopoverArrow,
  PopoverClose,
} from "@tessinaui/ui";
example.tsx
<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:

example.tsx
<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.

example.tsx
<Popover>
  <PopoverTrigger>…</PopoverTrigger>
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle />
      <PopoverDescription />
    </PopoverHeader>
    <PopoverBody />
    <PopoverSeparator />
    <PopoverFooter />
    <PopoverArrow />
  </PopoverContent>
</Popover>

Size

SizeMin widthMax widthText
"sm"200px240pxxs
"md" (default)260px320pxsm
"lg"320px400pxsm
"xl"400px480pxbase

Intent

Applies a semantic border colour to the popup and arrow — useful for warning/error confirmations.

ValueBorder
"none" (default)border-border
"error"border-error
"warning"border-warning
"success"border-success
"info"border-info

Rounded

ValueCSS
"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.

example.tsx
<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:

example.tsx
<Popover modal>
  …
</Popover>

To dim the background, render <PopoverBackdrop /> inside the popover:

example.tsx
<Popover modal>
  <PopoverTrigger render={<Button>Delete</Button>} />
  <PopoverBackdrop />
  <PopoverContent>…</PopoverContent>
</Popover>

Controlled

Popover accepts open, defaultOpen, and onOpenChange.

example.tsx
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.

example.tsx
<PopoverHeader showClose>
  <PopoverTitle>Rename file</PopoverTitle>
</PopoverHeader>

API Reference

Popover (root)

PropTypeDefaultDescription
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"inheritedReading direction. Unset, read off the trigger when the popup opens
openboolean—Controlled open state
defaultOpenbooleanfalseUncontrolled initial state
onOpenChange(open, event) => void—Fired when open state changes
modalbooleanfalseTrap focus and block page interaction
delaynumber—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

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Which side of the trigger to position on
align"start" | "center" | "end""center"Alignment along the side axis
sideOffsetnumber18Distance in px from the trigger. With arrow, floored at 21 (13px tail + 8px air)
arrowbooleanfalseRender the speech-bubble tail pointing at the trigger
collisionAvoidanceobject—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
alignOffsetnumber0Shift along the side axis
portalbooleantrueRender 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
containerHTMLElement | nulldocument.bodyPortal 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
collisionPaddingnumber8Minimum gap from the viewport edges
positionerClassNamestring—Extra classes on the positioner — raise its z-50 when the popover sits in a higher stacking context
size, rounded, intent—inheritedOverride the root's variant

PopoverHeader

PropTypeDefaultDescription
iconReactNode—Leading icon rendered beside the title
trailingReactNode—Trailing slot on the title row — a badge, count, or status
showClosebooleanfalseRender 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] and data-[ending-style] plus position-aware data-[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 intent changes.

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.
  • PopoverTitle maps to aria-labelledby, PopoverDescription to aria-describedby.
  • Focus moves to the first focusable element in the popup on open, and returns to the trigger on close.
  • Escape closes the popover, and so does a click outside it. To keep it open through both, control open yourself and ignore the dismissal in onOpenChange — the callback's reason tells 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>.

SidebarHoverCard

On this page

When to useWhen not to useInstallationUsageExamplesDefaultSizesIntentsRoundedCurving your own contentPositioningConfirmationScrollableFilter panelSkeletonOn-colorAnatomySizeIntentRoundedPositioningArrowBackdrop & modal behaviourControlledClose button in headerAPI ReferencePopover (root)PopoverContentPopoverHeaderPopoverTrigger, PopoverCloseSub-componentsNotesAccessibilityRTL