Popover
A non-modal panel anchored to its trigger — for content that is richer than a tooltip but lighter than a dialog.
Usage#
import { Popover, PopoverContent, PopoverTrigger } from "neelam-ui";
<Popover>
<PopoverTrigger>Dimensions</PopoverTrigger>
<PopoverContent>
<label>Width <Input size="sm" defaultValue="480" /></label>
</PopoverContent>
</Popover>Popover, dialog, or tooltip?#
Tooltip— a few words of label. Never focusable, never interactive.- Popover — interactive content the user can work through, without the page behind being blocked.
Dialog— an interruption that must be resolved before anything else. Traps focus and inerts the page.
PopoverContent is role="dialog", not role="menu" or "listbox" — its
content is arbitrary rather than a list of choices. For a list of actions, use
DropdownMenu.
Naming the panel#
A role="dialog" needs an accessible name; axe's aria-dialog-name rule flags
its absence. Rather than making every caller remember that, PopoverContent
defaults aria-labelledby to the trigger's own id — reusing whatever already
names the trigger, for free.
Pass aria-label or aria-labelledby explicitly when the trigger's label does
not double as a good name for the panel:
<PopoverTrigger aria-label="Account">
<Avatar … />
</PopoverTrigger>
<PopoverContent aria-label="Account settings">…</PopoverContent>What the platform provides#
PopoverContent is a native popover (popover="auto"), so Escape and
outside-click dismissal come from the browser, and top-layer rendering means the
panel escapes any overflow: hidden ancestor without a portal.
On open, focus moves to the first focusable element inside — a form's first field — or to the panel itself when there is nothing focusable, as with a purely informational card. On close it returns to the trigger, through every dismissal path. The page behind is scroll-locked while the panel is open, so it cannot drift away from its anchor.
Non-modal, but scroll-locked
Scroll locking is positional, not a focus trap. Tab still leaves the popover
and continues into the page — which is the difference between this and a
Dialog.
Keyboard#
| Key | Behaviour |
|---|---|
| EnterSpace | Toggles the popover when focus is on the trigger. |
| TabShift+Tab | Moves through the panel's contents, then out into the page — focus is not trapped. |
| Escape | Closes the popover and returns focus to the trigger. |
Accessibility#
- The trigger is a native
<button>witharia-haspopup="dialog",aria-expanded, andaria-controlspointing at the panel — the WAI-ARIA non-modal dialog pattern. - The panel is
role="dialog"and always resolves to an accessible name, from the trigger by default. - Focus moves in on open and back to the trigger on close, including on outside click, where it is easiest to lose.
- Open and close transitions are dropped under
prefers-reduced-motion.
API reference#
Popover#
| Prop | Type | Default |
|---|---|---|
defaultOpen | boolean | false |
onOpenChange | ((open: boolean) => void) | — |
openControls the open state. Omit to let the popover manage its own state. | boolean | — |
PopoverTrigger#
Adds no props of its own — it renders a native <button> and accepts every
<button> attribute. The ARIA wiring (aria-haspopup, aria-expanded,
aria-controls) is applied for you.
PopoverContent#
Adds no props of its own beyond the native <div> attributes. aria-label and
aria-labelledby are the two worth knowing about — see
Naming the panel.
PopoverClose#
Renders a native <button> that closes the popover. Accepts every <button>
attribute.