Popover
Anchored floating panel with outside-click and Escape dismissal.
import { Popover, PopoverContent, PopoverTrigger } from '@elirobinson/react/components/organisms/Popover';Styles: @elirobinson/react/styles/organisms/Popover.css — already included when you import @elirobinson/react/styles.css.
Show code
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@elirobinson/react/components/organisms/Popover';
export default function Basic() {
return (
<Popover>
<PopoverTrigger className="ds-button ds-button--secondary">
What's included?
</PopoverTrigger>
<PopoverContent>
<p>Every coaching guide ships as a versioned PDF plus a printable practice-plan card.</p>
</PopoverContent>
</Popover>
);
}When to use it
Use Popover for content that's genuinely optional to see — extra detail, a small set of
filters, a short form — anchored to whatever triggered it. Nothing about it blocks the page: no
focus trap, no modal backdrop, background content stays fully interactive. If dismissing it
needs to be unmissable, or it needs to hold focus, that's a Dialog, not a Popover.
Controlled, with a manual dismiss
There's no PopoverClose — unlike Dialog, closing programmatically means calling
onOpenChange(false) yourself from inside the content.
Show code
import { useState } from 'react';
import { Button } from '@elirobinson/react/components/atoms/Button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@elirobinson/react/components/organisms/Popover';
const sports = ['All sports', 'Soccer', 'Basketball', 'Track'];
export default function Controlled() {
const [open, setOpen] = useState(false);
const [sport, setSport] = useState('All sports');
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger className="ds-button ds-button--secondary">Filter: {sport}</PopoverTrigger>
<PopoverContent>
<div className="demo-col">
{sports.map((option) => (
<Button
key={option}
variant={option === sport ? 'primary' : 'ghost'}
onClick={() => setSport(option)}
>
{option}
</Button>
))}
<Button variant="secondary" onClick={() => setOpen(false)}>
Done
</Button>
</div>
</PopoverContent>
</Popover>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | — |
onOpenChange | ((open: boolean) => void) | — | — |
open | boolean | — | — |
PopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | — | `'start'` lines the panel's left edge up with the trigger's and gives it the trigger's width as a minimum — the menu/listbox shape. `'end'` is the same shape mirrored: the panel's *right* edge is pinned to the trigger's, which is what a trigger near the right edge of the viewport needs. `'center'` centres it on the trigger and leaves the width to the content, which is what a tooltip wants. Default `'start'`. |
side | "top" | "bottom" | — | Which edge of the trigger the panel hangs from. Default `'bottom'`. |
Also accepts all HTMLAttributes<HTMLDivElement> & Pick<AnchoredOverlayContentProps, 'side' | 'align'> props.
PopoverTrigger
No props of its own beyond the inherited HTML attributes.
Also accepts all ButtonHTMLAttributes<HTMLButtonElement> props.
Accessibility
PopoverContentrendersrole="dialog", portaled todocument.body. This is a non-modal dialog role: nothing callsshowModal(), there's no focus trap, and the rest of the page stays interactive — the opposite ofDialog.Escapecloses it from anywhere (a document-level listener, the sameuseEscapeKeyhookDropdownMenuuses), and clicking outside the trigger or content closes it too.- No initial-focus management: opening the popover doesn't move DOM focus into
PopoverContent. Focus stays wherever it was — typically the trigger, since activating it doesn't blur it. If your content includes interactive elements a fully keyboard-driven flow depends on, move focus there yourself, or rely on the user tabbing in. - No built-in close affordance: there's no
PopoverClosesubcomponent. Closing it from inside the content means callingonOpenChange(false)yourself. - Content is positioned with fixed coordinates computed from the trigger's bounding rect,
the same anchoring mechanism
DropdownMenuuses — recalculated on scroll and resize while the popover is open, so the content follows its trigger. - Content that doesn't fit below its trigger flips above it, and content that overruns the
right edge pins its right edge to the trigger's instead — the
sideandalignyou pass toPopoverContentare preferences, not guarantees. Each axis flips at most once per open and never flips back, so it can't oscillate while you scroll. - Content that fits on neither side is shifted rather than shrunk: it keeps its width and
slides along the axis until it is back inside the viewport, so it never reflows into whatever
space is left beside a pinned edge. It stops being edge-aligned with its trigger, and may
overlap it, but it doesn't narrow. Content taller than the viewport takes a
max-heightof the viewport and scrolls instead, since no offset makes that fit. UnlikeDropdownMenu,PopoverContentsets nomin-widthof its own, so the trigger's width is the only floor — give your content--anchored-min-widthif it needs a wider one. SeeuseAnchoredPosition.
Do
- Use Popover for supplementary, non-blocking content — a hint, a filter panel, a short form.
- Wire your own dismiss control inside PopoverContent when the content needs an explicit close action.
- Keep content lightweight — nothing here traps focus, so a long or complex form is a sign you actually want Dialog.
Don't
- Use it for anything that must block interaction with the rest of the page — reach for Dialog, an actual native modal.
- Assume focus moves into the content on open — it doesn't; test keyboard flows accordingly.
- Render PopoverContent or PopoverTrigger outside a Popover — it throws.