Skip to content

Popover

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/popover.json

The component lands in components/ui/popover.tsx and is yours to edit.

See it liveThe sizes and the four sides, with and without the arrow - in either theme, and in the accent of any product of the line.→

Both hold interactive content, so the choice is about where the content belongs and what it interrupts.

Reach for Popover when the content is about the control: a filter panel on a filter button, a colour picker on a swatch, a short form on an edit button. It appears beside its trigger, the page underneath stays live, and focus is not trapped.

Reach for Dialog when the content is the screen’s whole business for the moment, or when it is long enough that anchoring it beside a button is absurd.

Reach for Tooltip if the content is a label — a few words, nothing to click. A popover holds things you interact with; a tooltip holds a phrase.

<Popover>
<PopoverTrigger render={<Button />}>Filters</PopoverTrigger>
<PopoverPopup side="bottom" align="start" sideOffset={8}>
<PopoverTitle>Filters</PopoverTitle>
<PopoverDescription>Narrow the list down.</PopoverDescription>
<Button render={<PopoverClose />}>Done</Button>
</PopoverPopup>
</Popover>

PopoverPopup renders its own portal, positioner and arrow, so there is nothing to arrange around it.

side and align are a preference, not an instruction. Base UI measures the trigger and the panel and flips or shifts the popup when the preferred side does not fit, so a popover near the bottom of the window comes out above its trigger. That is the behaviour worth having — a popover that stays where it was told is a popover half off the screen.

The arrow follows: it is rotated to whichever side the popup actually landed on, not the side that was asked for.

It is not modal. What is under the panel stays in the accessibility tree and stays clickable. That is right for something beside a control and wrong for something the page has to wait on — for that, use Dialog or ConfirmDialog.

The trigger says whether it is open. aria-expanded is the only way a screen reader learns that this button revealed something rather than did something, and Base UI puts it there.

It is portalled. An anchored popup rendered in place is clipped by the first ancestor with overflow: hidden, which is where most of them go to die.

Give it a title. The popup’s accessible name comes from PopoverTitle; without one a screen reader announces “dialog” and nothing else.

No colour of its own. Every class is written in tokens, so the same list is correct in both themes and in every product’s accent — no dark: utilities anywhere in it.

Props

Read from the types in ui/popover.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

Popover

The root. Controlled with open and onOpenChange, or left to manage itself around a PopoverTrigger.

Base UI's Popover.Root, unchanged under dowel's name - its props are on its own page.

PopoverTrigger

What opens it, and what the popup is measured against.

Base UI's Popover.Trigger, unchanged under dowel's name - its props are on its own page.

PopoverClose

What closes it, for a button inside the panel.

Base UI's Popover.Close, unchanged under dowel's name - its props are on its own page.

PopoverPopup

The panel. Portalled and positioned, so it is not clipped by an ancestor with overflow: hidden - which is where most anchored popups go to die.

PropTypeDefault
sizesm | md | lg'md'
sidebottom | top | left | right | inline-end | inline-startPreferred side of the trigger. Base UI flips it when it does not fit, and defaults it to the bottom.
aligncenter | start | endAlignment along that side. Base UI centres it by default.
sideOffsetnumber | OffsetFunction8Distance from the trigger, in pixels.
arrowbooleantrueWhether to draw the arrow pointing back at the trigger.
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullWhere to portal to. Defaults to the raised host of the overlay this is opened inside (layer.tsx), and to the document body when there is none - either way not the element it was opened from, whose overflow would clip it. Pass an element to put it somewhere else, such as a container being screenshotted.

And every prop of Base UI's Popover.Popup, passed on.

PopoverArrow

The notch pointing back at the trigger. Base UI rotates it to whatever side the popup landed on, which is why the rotation is keyed off data-side rather than off the side that was asked for.

Every prop of Base UI's Popover.Arrow, passed on.

PopoverTitle

The heading. Base UI points the popup's aria-labelledby at it, so a popover with one is named for a screen reader without anyone arranging it.

Every prop of Base UI's Popover.Title, passed on.

PopoverDescription

The line under the heading, and the popup's aria-describedby.

Every prop of Base UI's Popover.Description, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 12 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/popover.tsx - the file a product copies - newest first.

  1. v0.40.0
    • fix hold every surface over the page to its room on both axes d0436a7
  2. v0.32.0breaking
    • feat a layer for popups inside overlays, a body that scrolls alone 5244cfd
  3. v0.26.1
    • fix shade a popup instead of staining the page behind it 9c7a96b
  4. v0.8.0
    • feat Dialog, ConfirmDialog, Drawer, Popover, PreviewCard, Tooltip 5dc3c06