Popover
Anchored, click-driven overlay for interactive contextual content. Built on Radix Popover.
Overview
Popover is the right primitive for anchored content that responds to an intentional click: date pickers, color pickers, mini forms, share menus, share/like inline actions. It is distinct from Tooltip (hover-only, text-only, never interactive) and HoverCard (hover-only previews of non-critical content). When the content is too long or warrants blocking the page, escalate to Dialog or Sheet.
The default surface is w-72, padded p-4, with a Radix-driven transform-origin so the open animation feels anchored to the trigger.
Preview
Installation
bash npx gremorie@latest add rx-popover bash pnpm dlx gremorie@latest add rx-popover bash yarn dlx gremorie@latest add rx-popover bash bunx --bun gremorie@latest add rx-popover Usage
import { Button } from "@gremorie/rx-forms";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@gremorie/rx-overlays";
export function Example() {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open</Button>
</PopoverTrigger>
<PopoverContent>
<p className="text-sm">Anchored interactive content.</p>
</PopoverContent>
</Popover>
);
}Angular edition planned for Phase 5h. Star the repo to track progress.
API
<Popover>
Extends Radix Popover.Root. Notable props:
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled open state. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean) => void | - | Fired when open state changes. |
modal | boolean | false | When true, locks body scroll and outside interaction. |
<PopoverTrigger>
Extends Radix Popover.Trigger. Pair with asChild to forward styles to a Button or any custom trigger.
<PopoverContent>
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "center" | Alignment relative to the trigger axis. |
sideOffset | number | 4 | Pixel gap between trigger and content. |
side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side; flips automatically when there is no room. |
alignOffset | number | 0 | Offset along the alignment axis. |
avoidCollisions | boolean | true | Auto-reposition to stay in the viewport. |
collisionPadding | number | object | 0 | Distance from viewport edges to maintain. |
Wrapped in a Radix Portal. All other Radix Popover.Content props are forwarded.
<PopoverAnchor>
Optional. Anchors the content to an element different from the trigger. Useful when the visual anchor and the click target are not the same element.
<Popover>
<PopoverAnchor asChild>
<span>Visual anchor</span>
</PopoverAnchor>
<PopoverTrigger asChild>
<Button>Click me</Button>
</PopoverTrigger>
<PopoverContent>Anchored to the span, not the button.</PopoverContent>
</Popover><PopoverHeader>, <PopoverTitle>, <PopoverDescription>
Optional layout helpers. Header stacks title and description; title is font-medium; description is text-muted-foreground.
Composition
<Popover>owns the open/close state.<PopoverTrigger asChild>wraps the focusable element that opens it.<PopoverContent>mounts via Portal, anchored to the trigger (orPopoverAnchor).- Inside content: any interactive UI - forms, command lists, color pickers, sliders.
- Dismissal: click outside, press
Esc, or close programmatically.
Variations
Settings popover
Mini form with header. Stays open while the user toggles preferences.
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Quick settings</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Quick settings</PopoverTitle>
<PopoverDescription>
Tweak preferences for this session.
</PopoverDescription>
</PopoverHeader>
<div className="mt-3 flex flex-col gap-3">
<Field>
<FieldLabel htmlFor="theme">Theme</FieldLabel>
<Select id="theme" defaultValue="system">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</Select>
</Field>
</div>
</PopoverContent>
</Popover>Aligned to trigger end
Right-align the popover under a toolbar icon so it doesn't overflow the viewport.
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" aria-label="More">
<MoreHorizontalIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" sideOffset={8}>
{/* menu content */}
</PopoverContent>
</Popover>With custom anchor
Use PopoverAnchor to decouple the visual anchor from the click target (common in selection-driven UIs where a highlighted range opens the popover).
<Popover open={open} onOpenChange={setOpen}>
<PopoverAnchor virtualRef={selectionRef} />
<PopoverContent>Formatting toolbar</PopoverContent>
</Popover>Accessibility
- Role:
role="dialog"for the content (Radix default). - Keyboard:
Esccloses;Tabcycles focus within the content; click outside closes (configurable). - Focus management: focus moves into the content on open and returns to the trigger on close.
- Outside click: closes by default. Disable via
onInteractOutsideif the popover must persist. aria-expanded: set on the trigger automatically;aria-controlslinks to the content id.- Reduced motion: open/close animations honor
prefers-reduced-motion.
Related
- Tooltip - hover-only, non-interactive text label.
- Hover Card - hover-only rich preview, non-interactive.
- Dropdown Menu - menu-semantics popover with arrow navigation.
- Dialog - blocking modal when content is too large or critical.