Components

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.

Updated Oct 8, 2026 by Juan Pablo Turina

Anatomy

  1. 1
    Popover (root)

    Radix Root that manages open/close state. Uncontrolled by default.

  2. 2
    PopoverTrigger

    Any focusable element. Pass asChild so the Popover wraps YOUR element instead of adding its own.

  3. 3
    PopoverAnchor

    Optional. Anchors the content to a different element than the trigger (advanced positioning).

  4. 4
    PopoverContent

    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.

Closed / Open
<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">Open</Button>
  </PopoverTrigger>
  <PopoverContent>Content</PopoverContent>
</Popover>
Use Popover for feature Tutorials
Yuno's onboarding pattern for new features and sections lives in a Popover — not a Dialog and not a Tooltip. The 'Feature tutorial' recipe (Skip / Step X of N / Got it!) is the canonical shape. Ship every new feature announcement with this composition so users get a consistent, dismissible signal.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Feature tutorial (Skip · Step X of N · Got it!)

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>
  );
}
Onboarding tip (single Got it!)

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>
Settings form (inline dimensions)

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>
Notifications tray

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>
Inside a Sheet or Dialog (container prop)

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.

PopoverAnchor is also exported for advanced positioning.
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";

Props

Everything else from the underlying HTML or Radix primitive is forwarded via ...props.

PropTypeDefaultDescription
Popover.open / defaultOpenboolean—Controlled or uncontrolled open state. Controlled needs onOpenChange.
Popover.onOpenChange(open: boolean) => void—Fires whenever the panel opens or closes.
Popover.modalbooleanfalseWhen true, blocks interaction with outside elements — behaves closer to a Dialog.
PopoverTrigger.asChildbooleanfalseMerges 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.sideOffsetnumber4Distance in px between trigger and content.
PopoverContent.classNamestring—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.containerHTMLElement | nulldocument.bodyDOM 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.
PopoverAnchorcomponent—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

Do
  • 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
  • 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.