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.
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.
Group
Sibling popovers in a Popover.Group close each other, and Tab moves
between them.
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}.
Props
Popover
PopoverProps extends Headless UI Popover props (minus as). children
may be ({open, close}) => ReactNode.
| Prop | Type | Description |
|---|---|---|
className | string | Merged last onto the relative root |
Popover.Button
PopoverButtonProps<TTag> is Headless UI PopoverButtonProps<TTag>
(as, disabled, autoFocus, …).
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'button' | Fragment merges the trigger props onto the single child; a component types its props |
disabled | boolean | false | data-disabled, pointer-events none, 50% opacity |
className | string | — | Merged last |
Popover.Panel
| Prop | Type | Default | Description |
|---|---|---|---|
anchor | Placement | {to, gap, offset, padding} | false | 'bottom start' | Portalled placement, or false to render in place below the button |
gap | number | string | 1 spacing unit | Button-to-panel space, written to --anchor-gap; a number is pixels. An anchor object's own gap wins |
transition | boolean | true | Scale + fade through data-closed / data-enter / data-leave |
focus | boolean | — | Move focus into the panel when it opens |
modal | boolean | — | Lock page scroll while open (the page is not made inert) |
static | boolean | — | Always render, ignoring open state (an anchored panel still renders nothing on the server) |
unmount | boolean | — | Keep the panel mounted (hidden) while closed |
style | CSSProperties | — | Merged after the gap variable |
className | string | — | Merged last |
Popover.Backdrop
| Prop | Type | Default | Description |
|---|---|---|---|
transition | boolean | true | Fade through data-closed |
className | string | — | Merged last |
Popover.Group
| Prop | Type | Description |
|---|---|---|
className | string | Merged 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.
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'button' | |
className | string | — | Merged last |
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="popover"] | div | Root, with Headless UI data-open |
[data-slot="popover-button"] | button (or as) | data-open, data-disabled, data-focus, data-hover |
[data-slot="popover-panel"] | div | bg-popover, shadow-medium; data-open / data-closed / data-anchor |
[data-slot="popover-backdrop"] | div | Fixed 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
| Token | Used for |
|---|---|
--popover, --popover-foreground | Panel surface and text |
--border | Panel border |
--shadow-medium | Panel elevation |
--radius-md | Panel corners |
--foreground | Backdrop scrim (at 50%) |
--ring | Button and close focus ring |
--duration-normal | Open / close transition |
Accessibility
- The button gets
aria-expandedandaria-controlsfrom 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. focusmoves 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;
modalonly locks scroll. Use Dialog when the task must be completed before continuing. - Transitions respect the shared
motion-reducerules of the surface classes.