Popover is a thin wrapper around Radix’s popover primitive. It renders a
floating panel anchored to a trigger, opens on click or keyboard
activation, and stays open until the user dismisses it via Escape, a
click outside, or a controlled close. Use it for short, self-contained
interactions that belong close to their trigger: renaming a resource,
picking a colour, confirming a quick action, toggling a cluster of
filters.
Popover and Tooltip look similar but play different roles. A Tooltip is hover-only, non-interactive, and disappears the moment the pointer leaves the trigger. It’s for supplementary labels, never load-bearing content. A Popover is keyboard-reachable, traps focus inside its panel, and is the right home for inputs, links, and buttons. If a user needs to read at their own pace or interact with what’s inside, reach for Popover.
Import
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@unkey/ui";
Basic
Wrap a trigger in <PopoverTrigger asChild> so the Button remains the
real focusable element, then pair it with a <PopoverContent>. Anything
inside the content is interactive and participates in the focus ring.
Positioning
side picks which edge of the trigger the panel anchors to (top,
right, bottom, left), align controls cross-axis alignment
(start, center, end), and sideOffset sets the pixel gap between
trigger and content. Radix flips the panel automatically when there
isn’t room on the requested side, so side is a preference, not a
guarantee.
Controlled
Pass open and onOpenChange when the parent needs to drive visibility.
Useful when a popover has to open in response to a distant event (a
keyboard shortcut, the result of a mutation) or when two unrelated
triggers share the same panel.
Accessibility
Radix handles the fiddly parts. The trigger carries
aria-expanded, aria-controls, and aria-haspopup="dialog", so screen
readers announce it as a collapsed panel. When the popover opens, focus
moves into the content and is trapped there until the user closes it.
Escape closes the panel and returns focus to the trigger, an outside
click closes without stealing focus, and Tab cycles through focusable
elements inside the panel.
Because a Popover is a real dialog, never use it for passive information. If the content is just a label with no interactive element, a Tooltip is the right primitive and plays better with screen readers. Conversely, don’t use a Popover for a blocking decision (delete, logout) where the user must not miss the choice. That’s what a Dialog is for.
Props
Popover
The root state container. Controls a single popover’s open/close state.
defaultOpen boolean Optional Whether the popover is open on first render. Use for uncontrolled popovers that should start open (rare — popovers are almost always dormant until the trigger is activated).
open boolean Optional Controlled open state. Pair with onOpenChange when the parent needs
to drive visibility.
onOpenChange (open: boolean) => void Optional Fires whenever the open state changes, whether by click, keyboard, outside click, escape, or programmatic control.
modal boolean Optional
Default false When true, the popover traps focus and renders an inert overlay
over the rest of the page. Use for short confirmations where the
surrounding UI should not be interactive while the panel is open.
children ReactNode Optional A <PopoverTrigger> and a <PopoverContent>.
PopoverTrigger
The element that, when clicked or activated with Space / Enter,
toggles the popover. Almost always rendered with asChild so an existing
control (Button, IconButton, link) keeps its own semantics and focus ring.
asChild boolean Optional
Default false Merge the trigger’s behavior into its single child instead of
rendering a wrapping <button>. Use this to keep a Button as the
focusable element.
children ReactNode Optional The trigger content. With asChild, must be a single element that
accepts a ref and standard pointer/focus handlers.
PopoverContent
The floating panel. Rendered into a portal so it escapes overflow: hidden ancestors, with default chrome (bg-gray-2, border, rounded
corners, drop shadow, w-72, open/close animation).
side "top" | "right" | "bottom" | "left" Optional
Default "bottom" Preferred side of the trigger to anchor to. Radix flips automatically when the requested side doesn’t fit in the viewport.
align "start" | "center" | "end" Optional
Default "center" Alignment on the cross axis. With side="bottom", start means
left-aligned with the trigger, end means right-aligned.
sideOffset number Optional
Default 4 Pixel gap between the trigger and the panel on the chosen side.
alignOffset number Optional
Default 0 Pixel shift along the cross axis, relative to the chosen align.
avoidCollisions boolean Optional
Default true When true, Radix flips or shifts the panel to stay inside the
viewport. Set to false only if you’ve already constrained layout.
onOpenAutoFocus (event: Event) => void Optional Fires when focus moves into the panel on open. Call
event.preventDefault() to keep focus on the trigger instead of
auto-focusing the first element inside.
onCloseAutoFocus (event: Event) => void Optional Fires when focus is about to return to the trigger on close. Call
event.preventDefault() to redirect focus elsewhere.
onEscapeKeyDown (event: KeyboardEvent) => void Optional Fires when the user presses Escape while the panel is open.
Prevent to keep the popover open.
onPointerDownOutside (event: PointerDownOutsideEvent) => void Optional Fires when the user clicks outside the panel. Prevent to keep the popover open (for example, while a nested picker is active).
className string Optional Additional Tailwind classes. Merged with the default chrome via cn.
Common overrides are width (w-96) and padding.
children ReactNode Optional The panel content. Anything interactive — inputs, links, buttons — belongs here.
Related
- Tooltip — for a short, non-interactive label that appears on hover or focus, never on click.
- Dialog — when the interaction is blocking or destructive and must interrupt the user’s flow.
- ConfirmPopover — a pre-built confirm-and-cancel popover for quick inline confirmations.