Popover
A non-modal panel anchored to its trigger, rendered as a <div popover> in the
top layer. The browser handles stacking, light dismiss and Escape; CSS anchor
positioning handles the placement.
import { Popover } from '@apostel/bedrock'
// or, for a veto: import { Popover } from '@apostel/bedrock/controlled'
Source
import { Popover } from '../../src/index'
/**
* `side` and `align` become CSS anchor positioning, so the panel follows the
* trigger on scroll with no listener and no measuring. Escape and clicking
* outside are the platform's light dismiss.
*/
export default function PopoverDemo() {
return (
<Popover.Root>
<Popover.Trigger>Filters</Popover.Trigger>
<Popover.Content side="bottom" align="start" sideOffset={8}>
<div className="demo-row">
<label>
<input type="checkbox" defaultChecked /> Open
</label>
</div>
<div className="demo-row">
<label>
<input type="checkbox" /> Archived
</label>
</div>
<Popover.Close>Done</Popover.Close>
</Popover.Content>
</Popover.Root>
)
}Anatomy
<Popover.Root>
<Popover.Trigger />
<Popover.Anchor />
<Popover.Content>
<Popover.Close />
</Popover.Content>
</Popover.Root>
No Portal. A popover is in the top layer, so it paints above everything
regardless of where it sits in the tree or what its ancestors set overflow to.
Popover.Root
Renders no element. See two roots.
| prop | type | notes |
|---|---|---|
kind |
'auto' | 'manual' | 'hint' |
Defaults to auto. |
onOpenChange |
(open: boolean) => void |
Reports. Cannot refuse — use /controlled. |
kind is named for behaviour, not for the popover attribute it currently maps
onto one-for-one:
| value | behaviour |
|---|---|
auto |
Light-dismisses, and closes other auto popovers when it opens. |
manual |
Neither. You close it yourself. |
hint |
Layers above an open menu instead of closing it. See browser support. |
There is no defaultOpen. A popover cannot be shown before its element is
connected, and calling showPopover() from a mount effect is exactly the class
of thing this library exists to delete. Use open from /controlled instead.
Popover.Trigger
Renders <button type="button" commandfor command="toggle-popover">.
| prop | type | notes |
|---|---|---|
asChild |
boolean |
The child must render a <button>. Enforced at mount. |
Unlike Dialog.Trigger, aria-expanded is not written by hand — Chrome
gives a popover invoker that attribute implicitly. Writing it too would mean two
sources for one piece of state.
Popover.Anchor
Renders a <div>. Optional, and only needed when the thing the panel should
point at is not the thing that opens it.
Position it against a row while the button lives in that row's corner:
<Popover.Root>
<Popover.Anchor>
<span>Row label</span>
<Popover.Trigger>Edit</Popover.Trigger>
</Popover.Anchor>
<Popover.Content>…</Popover.Content>
</Popover.Root>
Popover.Content
Renders <div popover data-bedrock-popover>.
| prop | type | notes |
|---|---|---|
side |
Side |
'top' | 'right' | 'bottom' | 'left'. Default bottom. |
align |
Align |
'start' | 'center' | 'end'. Default center. |
sideOffset |
number |
Pixels between anchor and panel. |
avoidCollisions |
boolean |
Default true. Becomes position-try-fallbacks. |
id |
— | Not forwarded. The trigger's commandfor points at it. |
asChild |
boolean |
Supported. |
side and align compile to position-area plus self-alignment, so the panel
follows its anchor on scroll and resize with no listener, no measuring and no
re-render. avoidCollisions becomes position-try-fallbacks, which flips it
when it would overflow the viewport.
Where a browser has no anchor positioning, the panel still opens and still dismisses — it lands where the UA puts a popover, centred, rather than beside the trigger. See browser support for what degrades and how.
Children mount only while it is open, on beforetoggle. Closing discards
whatever the panel was holding, which is what makes the uncontrolled root
uncontrolled: a half-filled filter form resets because it is gone, not because
something reset it.
Popover.Close
Renders <button type="button" commandfor command="hide-popover">. Takes
asChild under the same button rule as Trigger.
usePopoverTrigger()
Returns { commandfor, command } to spread onto an element bedrock will not
render for you. Must be called inside a Popover.Root. No validation and no
accessibility guarantees — the same deal as useDialogTrigger().
Keyboard
| key | behaviour |
|---|---|
Enter/Space |
On the trigger, toggles. Native button activation. |
Escape |
Closes an auto or hint popover. Platform light dismiss. |
Tab |
Moves through the panel and then out of it. Not trapped. |
Focus is not moved into the panel on open, and not restored on close. A popover is not modal; moving focus would be wrong for the common case of a panel that sits beside the control that opened it.