Popover
Radix-powered floating panel anchored to a trigger. w-72 rounded-md bg-popover shadow-md p-4. Used for supplementary content (settings snippets, help, notifications) that doesn't warrant a Dialog. Also the canonical surface for feature Tutorial popovers.
Anatomy
- 1Popover (root)
Radix Root that manages open/close state. Uncontrolled by default.
- 2PopoverTrigger
Any focusable element. Pass asChild so the Popover wraps YOUR element instead of adding its own.
- 3PopoverAnchor
Optional. Anchors the content to a different element than the trigger (advanced positioning).
- 4PopoverContent
The floating panel: w-72 rounded-md border bg-popover shadow-md p-4, portalled (by default to document.body; pass `container` to portal elsewhere), animated in via animate-dialog-in.
States
Open / Closed. Focus is trapped inside the panel while open; clicking outside or pressing Esc closes it. Portal escapes any overflow-hidden ancestor. When the trigger sits inside a modal Sheet / Dialog, pass `container` on PopoverContent — otherwise mouse wheel over the popover's inner scroll list is blocked by Radix Dialog's react-remove-scroll wrapper.
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open</Button>
</PopoverTrigger>
<PopoverContent>Content</PopoverContent>
</Popover>Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
The Yuno pattern for explaining a new feature in a few steps: a TourCard inside the Popover. The card is the design system's own (title, close X, one line of body, Skip or Back, the step count, Next and then Got it!), so the popover only places it and drops its own surface.
function TutorialPopover() {
const [step, setStep] = React.useState(1);
const total = 3;
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Show tutorial</Button>
</PopoverTrigger>
{/* The card brings its own surface; the popover only places it. */}
<PopoverContent align="start" className="w-92 border-0 bg-transparent p-0 shadow-none">
<TourCard
title="Tutorial title"
body="Say what the new feature does in one sentence."
step={step}
total={total}
onNext={() => setStep((s) => s + 1)}
onBack={() => setStep((s) => s - 1)}
onDismiss={close}
/>
</PopoverContent>
</Popover>
);
}A one-step tip on one control: the same TourCard with no step count and no Skip, just Got it! and the close X. Gone in one click.
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Show tip</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-92 border-0 bg-transparent p-0 shadow-none">
<TourCard
title="Customize your table"
body="Now you can rearrange, hide and resize the columns."
onNext={close}
onDismiss={close}
/>
</PopoverContent>
</Popover>Small form fields inside a popover, launched from a control button. Use for quick tweaks that don't warrant leaving the current context.
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Dimensions</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-80">
<div className="grid gap-3">
<div className="grid gap-1">
<div className="text-base font-medium text-foreground">Dimensions</div>
<p className="text-sm text-muted-foreground">Set the dimensions for the layer.</p>
</div>
<div className="grid grid-cols-3 items-center gap-3">
<Label htmlFor="width" className="text-right">Width</Label>
<Input id="width" defaultValue="100%" className="col-span-2 h-8" />
</div>
<div className="grid grid-cols-3 items-center gap-3">
<Label htmlFor="maxWidth" className="text-right">Max. width</Label>
<Input id="maxWidth" defaultValue="300px" className="col-span-2 h-8" />
</div>
</div>
</PopoverContent>
</Popover>Bell IconButton opens a wider popover with a scrollable list of notifications. Common in the top bar utilities cluster.
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Notifications">
<BellSimple weight="light" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-80 p-0">
<div className="border-b px-4 py-3 text-sm font-medium">Notifications</div>
<ul className="grid gap-0.5 p-2">
{items.map(n => (
<li key={n.id} className="rounded-sm p-3 text-sm hover:bg-accent">
<div className="text-foreground">{n.title}</div>
<div className="text-xs text-muted-foreground">{n.time}</div>
</li>
))}
</ul>
</PopoverContent>
</Popover>When a Popover lives inside a modal Sheet / Dialog, portal it INTO the modal's Content element (pass `container` = closest `[role='dialog']` ancestor of the trigger). Without this the popover portals to document.body which sits OUTSIDE Radix Dialog's react-remove-scroll wrapper, and any wheel event over the popover's internal scroll list is swallowed. Applies to Combobox / DropdownMenu / any popover with a scrollable list opened from a Sheet.
function InsideSheetRecipe() {
const triggerRef = React.useRef<HTMLButtonElement>(null);
const [popoverOpen, setPopoverOpen] = React.useState(false);
const [container, setContainer] = React.useState<HTMLElement | null>(null);
// On open, resolve the closest modal ancestor as the portal container.
React.useEffect(() => {
if (!popoverOpen) return;
setContainer(
triggerRef.current?.closest<HTMLElement>('[role="dialog"]') ?? null,
);
}, [popoverOpen]);
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Sheet with Popover inside</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader><SheetTitle>Container prop demo</SheetTitle></SheetHeader>
<div className="p-6">
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild>
<Button ref={triggerRef} variant="outline">
Open popover with 30 rows
</Button>
</PopoverTrigger>
<PopoverContent
container={container}
align="start"
className="p-0"
>
<div className="max-h-40 overflow-y-auto p-3">
{Array.from({ length: 30 }).map((_, i) => (
<p key={i} className="py-1 text-sm text-foreground">
Row {i + 1} — try the wheel here
</p>
))}
</div>
</PopoverContent>
</Popover>
</div>
</SheetContent>
</Sheet>
);
}Import
Copy this import line at the top of the file where you compose this atom.
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";Props
Everything else from the underlying HTML or Radix primitive is forwarded via ...props.
| Prop | Type | Default | Description |
|---|---|---|---|
| Popover.open / defaultOpen | boolean | — | Controlled or uncontrolled open state. Controlled needs onOpenChange. |
| Popover.onOpenChange | (open: boolean) => void | — | Fires whenever the panel opens or closes. |
| Popover.modal | boolean | false | When true, blocks interaction with outside elements — behaves closer to a Dialog. |
| PopoverTrigger.asChild | boolean | false | Merges props onto the child element (Radix Slot). Always pass when wrapping a real element (Button, IconButton). |
| PopoverContent.side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side. Radix auto-flips if there's no room. |
| PopoverContent.align | "start" | "center" | "end" | "center" | Alignment along the side. Tutorials usually use align='start'; notifications trays use align='end'. |
| PopoverContent.sideOffset | number | 4 | Distance in px between trigger and content. |
| PopoverContent.className | string | — | Merged. Default is w-72 p-4. Override w-* for wider panels (notifications, tutorials use w-80). Use p-0 when you handle padding per-section. |
| PopoverContent.container | HTMLElement | null | document.body | DOM element to portal into. When the trigger sits inside a modal Sheet / Dialog, pass the closest `[role='dialog']` ancestor so the popover lives INSIDE Radix Dialog's react-remove-scroll wrapper — otherwise mouse wheel over the popover's inner scroll list is blocked. See the 'Inside a Sheet or Dialog' recipe above. |
| PopoverAnchor | component | — | Optional anchor different from the trigger. Advanced — most Popovers don't need it. |
When to use
- Small forms or previews launched from a control.
- Notifications tray triggered by the bell IconButton.
- Feature tutorials and onboarding tips over new sections.
- Non-critical supplementary content anchored to a trigger.
When not to use
- For focused editing that needs the full page context — use Sheet or Dialog.
- For informational tooltips — use Tooltip.
- For critical decisions the user must not miss — use Dialog.
Usage
- Keep the content under 400px tall.
- Anchor it to a real trigger — never floating without one.
- For tutorials, always include Skip so users can dismiss the whole flow.
- For onboarding tips, provide a single primary Got it! so the action is unambiguous.
- When the trigger is inside a Sheet / Dialog, pass `container` on PopoverContent (the closest `[role='dialog']` ancestor) so mouse wheel over the inner list scrolls.
- Don't stack Popovers.
- Don't rely on a Popover for critical decisions — use Dialog.
- Don't hide destructive actions inside a Popover — they need a Dialog confirmation.
- Don't skip the animation — animate-dialog-in is the Yuno default.
- Don't leave the default portal-to-body when the popover is inside a modal Sheet / Dialog — wheel events over its inner scroll list will be blocked.
Related
Cross-links to atoms and patterns you may reach for next.
- DialogFor blocking decisions or critical modals that need the user's full attention.
- SheetFor focused editing that needs more room than a Popover.
- TooltipFor hover hints without interactive content.
- Hover cardFor preview-on-hover with richer content than a Tooltip.
- Dropdown menuWhen the panel should offer a list of actions instead of free-form content.