Popover
A floating content panel with arrow, close button, and configurable placement. Built on Radix UI.
Basic
A floating content panel that appears relative to a trigger element. Built on Radix UI with support for arrow indicators, close buttons, and 4-side placement.
Installation
Install the component via the CLI in one command.
npx @work-rjkashyap/unified-ui add popoverpnpm dlx @work-rjkashyap/unified-ui add popovernpx @work-rjkashyap/unified-ui add popoverbunx @work-rjkashyap/unified-ui add popoverIf you haven't initialized your project yet, run npx @work-rjkashyap/unified-ui init first. See the CLI docs for details.
Or install the full package
Use this approach if you prefer to install the entire design system as a dependency instead of copying individual components.
npm install @work-rjkashyap/unified-uipnpm add @work-rjkashyap/unified-uiyarn add @work-rjkashyap/unified-uibun add @work-rjkashyap/unified-uiAnatomy
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverClose,
PopoverArrow,
Button,
Input,
} from "@work-rjkashyap/unified-ui";Basic Usage
The Popover wraps Radix UI's Popover primitive with design system tokens. It uses a trigger-based pattern — wrap an interactive element in PopoverTrigger and the floating content in PopoverContent.
<Popover>
<PopoverTrigger asChild>
<Button>Open</Button>
</PopoverTrigger>
<PopoverContent>
<p>Popover content goes here.</p>
</PopoverContent>
</Popover>Placement
Control which side of the trigger the popover appears on using the side prop on PopoverContent. Fine-tune alignment with the align prop.
<PopoverContent side="top"> {/* Above the trigger */}
<PopoverContent side="right"> {/* To the right */}
<PopoverContent side="bottom"> {/* Below — default */}
<PopoverContent side="left"> {/* To the left */}
{/* Alignment within the side */}
<PopoverContent side="bottom" align="start"> {/* Left-aligned */}
<PopoverContent side="bottom" align="center"> {/* Centered — default */}
<PopoverContent side="bottom" align="end"> {/* Right-aligned */}| Side | Description |
|---|---|
top | Above the trigger element |
right | To the right of the trigger |
bottom | Below the trigger (default) |
left | To the left of the trigger |
| Align | Description |
|---|---|
start | Align to the start edge |
center | Center alignment (default) |
end | Align to the end edge |
With Arrow
Add a PopoverArrow inside PopoverContent to render a small pointing arrow toward the trigger.
Or use the PopoverArrow component directly for more control:
<PopoverContent>
<PopoverArrow />
<p>Content with explicit arrow component.</p>
</PopoverContent>With Close Button
Set showClose on PopoverContent to render a close button in the top-right corner.
Combined: Arrow + Close
<Popover>
<PopoverTrigger asChild>
<Button>Settings</Button>
</PopoverTrigger>
<PopoverContent showClose arrow>
<p>Full-featured popover with arrow and close button.</p>
</PopoverContent>
</Popover>With Form Content
Popovers are commonly used for inline editing, quick settings, and small forms.
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Edit Name</Button>
</PopoverTrigger>
<PopoverContent showClose>
<div className="flex flex-col gap-3">
<div className="space-y-1">
<h4 className="text-sm font-semibold">Edit display name</h4>
<p className="text-xs text-muted-foreground">
This name will be visible to other users.
</p>
</div>
<Input placeholder="Display name" size="sm" />
<div className="flex justify-end gap-2">
<PopoverClose asChild>
<Button variant="ghost" size="sm">
Cancel
</Button>
</PopoverClose>
<Button variant="primary" size="sm">
Save
</Button>
</div>
</div>
</PopoverContent>
</Popover>Offset and Collision
Use sideOffset to control the distance between the trigger and the popover. The popover automatically flips to avoid viewport collisions (handled by Radix UI).
{
/* Default offset */
}
<PopoverContent sideOffset={4}>...</PopoverContent>;
{
/* Larger offset */
}
<PopoverContent sideOffset={12}>...</PopoverContent>;Controlled
Use open and onOpenChange for programmatic control.
const [open, setOpen] = useState(false);
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button>Controlled</Button>
</PopoverTrigger>
<PopoverContent>
<p>This popover is controlled externally.</p>
<Button variant="primary" size="sm" onClick={() => setOpen(false)}>
Done
</Button>
</PopoverContent>
</Popover>;Color Picker Pattern
A common pattern using a popover for a color picker or swatch selector.
<Popover>
<PopoverTrigger asChild>
<button
className="size-8 rounded-md border border-border"
style={{ backgroundColor: color }}
aria-label="Choose color"
/>
</PopoverTrigger>
<PopoverContent side="bottom" align="start">
<div className="grid grid-cols-5 gap-2">
{colors.map((c) => (
<button
key={c}
className="size-6 rounded-sm border border-border"
style={{ backgroundColor: c }}
onClick={() => setColor(c)}
aria-label={c}
/>
))}
</div>
</PopoverContent>
</Popover>Props
Popover
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state. |
defaultOpen | boolean | — | Default open state (uncontrolled). |
onOpenChange | (open: boolean) => void | — | Callback when the open state changes. |
modal | boolean | false | Whether the popover acts as a modal (traps focus). |
PopoverTrigger
Uses Radix UI's PopoverTrigger API. Use asChild to render the trigger as your own element.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge props onto the child element instead of rendering a <button>. |
PopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "bottom" | Which side of the trigger to place the popover. |
align | "start" | "center" | "end" | "center" | Alignment along the side. |
sideOffset | number | 4 | Distance in pixels from the trigger. |
showClose | boolean | false | Whether to render a close button. |
arrow | boolean | false | Whether to render a pointing arrow. |
className | string | — | Additional CSS classes. |
PopoverClose
Uses Radix UI's PopoverClose API. Use asChild to render as your own element.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge props onto the child element. |
PopoverArrow
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes for the arrow SVG. |
Accessibility
Built on @radix-ui/react-popover which provides full ARIA compliance and keyboard management.
- Focus management — Focus moves into the popover when opened and returns to the trigger when closed.
- Escape to close — Pressing
Escapecloses the popover. - Click outside — Clicking outside the popover dismisses it (configurable via Radix).
- ARIA attributes — The trigger has
aria-expandedandaria-haspopup. Content hasrole="dialog". - Close button has an accessible
aria-label. - Focus ring is visible on keyboard navigation only (
focus-visible). - Uses
z-popoverz-index token to layer correctly above page content. - Collision detection automatically repositions the popover to stay within the viewport.
Design Tokens
| Token | Usage |
|---|---|
--surface-raised | Popover panel background |
--border | Popover border color |
--shadow-md | Popover elevation shadow |
--radius-md | Popover border radius |
--z-popover | Z-index for the popover layer |
--duration-fast | Enter/exit animation timing |
Drawer
A bottom drawer with native-feeling drag-to-dismiss interaction. Built on vaul with snap points, multiple sizes, and composable slot components.
Hover Card
A rich preview card that appears on hover. Built on Radix UI HoverCard with scaleIn animation, configurable open/close delay, and arrow indicator.