Skip to main content

Popover

A popover is free-form content anchored to a button: a filter form, a colour picker, a small settings sheet. Headless UI owns the open state (click, Enter, Space open; Escape and outside click close; focus returns to the button) and stamps data-open on the root, button and panel. For a list of actions use Menu instead.

import Popover from '@zuilib/components/popover'

Basic

Popover.Button is styled with a focus ring only, so wrap your own Button with as={Fragment} to keep its look. The panel is portalled to <body> and placed by anchor.

Loading example

Anchor and gap

anchor takes a Headless UI placement ('top', 'bottom end', 'right start', …) or an object {to, gap, offset, padding}. gap sets the space between button and panel (a number is pixels); the default is one spacing unit. anchor={false} renders the panel in place below the button inside the root instead of portalling it: it keeps a subtree theme and server-renders.

Loading example

Group

Sibling popovers in a Popover.Group close each other, and Tab moves between them.

Loading example

Backdrop, focus and render props

Popover.Backdrop dims the page with the same bg-foreground/50 scrim as Dialog and closes on click; it is rendered in place, so put it before the panel. focus moves focus into the panel on open; modal locks page scroll (the page is not made inert). The root's children may be a render function with {open, close}.

Loading example

Props

Popover

PopoverProps extends Headless UI Popover props (minus as). children may be ({open, close}) => ReactNode.

PropTypeDescription
classNamestringMerged last onto the relative root

Popover.Button

PopoverButtonProps<TTag> is Headless UI PopoverButtonProps<TTag> (as, disabled, autoFocus, …).

PropTypeDefaultDescription
asElementType'button'Fragment merges the trigger props onto the single child; a component types its props
disabledbooleanfalsedata-disabled, pointer-events none, 50% opacity
classNamestringMerged last

Popover.Panel

PropTypeDefaultDescription
anchorPlacement | {to, gap, offset, padding} | false'bottom start'Portalled placement, or false to render in place below the button
gapnumber | string1 spacing unitButton-to-panel space, written to --anchor-gap; a number is pixels. An anchor object's own gap wins
transitionbooleantrueScale + fade through data-closed / data-enter / data-leave
focusbooleanMove focus into the panel when it opens
modalbooleanLock page scroll while open (the page is not made inert)
staticbooleanAlways render, ignoring open state (an anchored panel still renders nothing on the server)
unmountbooleanKeep the panel mounted (hidden) while closed
styleCSSPropertiesMerged after the gap variable
classNamestringMerged last

Popover.Backdrop

PropTypeDefaultDescription
transitionbooleantrueFade through data-closed
classNamestringMerged last

Popover.Group

PropTypeDescription
classNamestringMerged last onto the flex items-center gap-2 row

Popover.Close

PopoverCloseProps<TTag> is Headless UI CloseButtonProps<TTag>; polymorphic like the button. Closes the nearest popover and returns focus to its button.

PropTypeDefaultDescription
asElementType'button'
classNamestringMerged last

Slots

SlotElementNotes
[data-slot="popover"]divRoot, with Headless UI data-open
[data-slot="popover-button"]button (or as)data-open, data-disabled, data-focus, data-hover
[data-slot="popover-panel"]divbg-popover, shadow-medium; data-open / data-closed / data-anchor
[data-slot="popover-backdrop"]divFixed scrim; rendered in place
[data-slot="popover-group"]div
[data-slot="popover-close"]button (or as)
/* Consumer CSS: wider panels with no padding */
[data-slot="popover-panel"] {
padding: 0;
min-width: 20rem;
}

Tokens

TokenUsed for
--popover, --popover-foregroundPanel surface and text
--borderPanel border
--shadow-mediumPanel elevation
--radius-mdPanel corners
--foregroundBackdrop scrim (at 50%)
--ringButton and close focus ring
--duration-normalOpen / close transition

Accessibility

  • The button gets aria-expanded and aria-controls from Headless UI; Enter and Space open it, Escape closes and returns focus to it.
  • Outside click closes; Tab out of the panel closes it and moves on. Inside a Popover.Group, Tab moves between sibling popovers.
  • focus moves focus into the panel on open; without it focus stays on the button and the panel is reachable by Tab.
  • The rest of the page stays interactive while the panel is open; modal only locks scroll. Use Dialog when the task must be completed before continuing.
  • Transitions respect the shared motion-reduce rules of the surface classes.
  • Menu: a list of actions with roving focus and typeahead.
  • Tooltip: a few words of hover / focus text.
  • Dialog: a modal overlay that makes the page inert.
  • Button: the trigger to wrap with as={Fragment}.