Popover shows a small floating panel of non-modal supplementary content anchored to a trigger element. Unlike Tooltip, it opens on click (not hover) and can contain interactive content — form controls, buttons, links. It shares the same floating-surface visual language as Select, DropdownMenu, and Tooltip.
Installation
HTML — already included if you loaded the 3 stylesheets (tokens.css, base.css, components.css) — see the Installation guide.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML
Operant ships no plain-HTML popover primitive — floating/portal positioning, focus trapping, and outside-click dismissal require JavaScript. Use the React version below, or wire up your own positioning logic and apply .ods-popover-panel + .ods-popover-arrow to the resulting panel.
<button class="ods-btn -secondary" id="popover-trigger" aria-expanded="false">
Open popover
</button>
<div class="ods-popover-panel" role="dialog" hidden>
<p>Some brief supplementary content, positioned relative to its trigger.</p>
</div>
React
import { Popover, PopoverTrigger, PopoverContent } from "@operant/ui";
export default function Example() {
return (
<Popover>
<PopoverTrigger className="ods-btn -secondary">Open popover</PopoverTrigger>
<PopoverContent>
<p>Some brief supplementary content, positioned relative to its trigger.</p>
</PopoverContent>
</Popover>
);
}
Examples
Basic
The default case: a trigger styled as a secondary button, and a panel with .ods-popover-panel (padding, elevated surface, border, shadow) plus .ods-popover-arrow pointing back at the trigger.
HTML
<button class="ods-btn -secondary" aria-expanded="false">Open popover</button>
<div class="ods-popover-panel" role="dialog">
<p>Some brief supplementary content, positioned relative to its trigger.</p>
</div>
React
<Popover>
<PopoverTrigger className="ods-btn -secondary">Open popover</PopoverTrigger>
<PopoverContent>
<p>Some brief supplementary content, positioned relative to its trigger.</p>
</PopoverContent>
</Popover>
With a close button
Add an explicit dismiss control using .ods-iconbtn inside the panel, positioned absolutely in a corner.
HTML
<div class="ods-popover-panel" role="dialog" style="position: relative;">
<button class="ods-iconbtn -sm" aria-label="Close"
style="position: absolute; top: var(--space-4); right: var(--space-4);">
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><!-- X icon --></svg>
</button>
<p>Dismiss this panel explicitly.</p>
</div>
React
import { Popover, PopoverTrigger, PopoverContent, PopoverClose } from "@operant/ui";
import { X } from "react-bootstrap-icons";
<Popover>
<PopoverTrigger className="ods-btn -secondary">Open popover</PopoverTrigger>
<PopoverContent style={{ position: "relative" }}>
<PopoverClose
className="ods-iconbtn -sm"
aria-label="Close"
style={{ position: "absolute", top: "var(--space-4)", right: "var(--space-4)" }}
>
<X />
</PopoverClose>
<p>Dismiss this panel explicitly.</p>
</PopoverContent>
</Popover>
Aligned to start
Pass align="start" to align the panel's start edge with the trigger's start edge instead of centering, and tighten sideOffset for a closer fit.
HTML
<!-- alignment/offset are Radix positioning props with no HTML/CSS-only
equivalent; documented as data attributes for reference only -->
<div class="ods-popover-panel" data-align="start" data-side-offset="4">
<p>Aligned to the start edge of the trigger.</p>
</div>
React
<Popover>
<PopoverTrigger className="ods-btn -secondary">Open (align start)</PopoverTrigger>
<PopoverContent align="start" sideOffset={4}>
<p>Aligned to the start edge of the trigger.</p>
</PopoverContent>
</Popover>
API Reference
Popover is a thin styled wrapper around @radix-ui/react-popover. Popover (Root) and PopoverTrigger are re-exported directly from Radix with no added class — PopoverTrigger deliberately has no .ods-* class of its own so it stays a real, unstyled <button>; apply .ods-btn/-<variant> via className yourself.
<PopoverContent>
| Prop | Type | Default | Description |
|---|---|---|---|
sideOffset | number | 8 | Pixel gap between the trigger and the panel. |
align | "start" | "center" | "end" | "center" | Alignment of the panel relative to the trigger. |
side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side to render the panel on. |
className | string | — | Merged with .ods-popover-panel. |
children | ReactNode | — | Panel content. |
Renders inside a Radix Portal automatically — consumers never need to manage portalling. Always renders PopoverPrimitive.Arrow with .ods-popover-arrow.
PopoverClose is a direct re-export of Radix's Close — render it inside PopoverContent for an explicit dismiss control.
HTML-side classes: .ods-popover-panel (the floating surface — elevated background, hairline border, radius-lg, shadow-md, max-width: 320px), .ods-popover-arrow (fills with --color-surface-elevated to match the panel).
Accessibility
- Radix Popover manages focus: opening moves focus into the panel, and closing (via
Esc, outside click, orPopoverClose) returns focus to the trigger. - The trigger is a real
<button>— keyboard-focusable and operable via Enter/Space with no extra wiring. Esccloses the popover; clicking outside the panel closes it too.- The panel is portalled to the document body, so it escapes any
overflow: hiddenancestor and stacks above other content via--z-dropdown. - Because
PopoverTriggerrenders no.ods-*class by default, remember to addaria-labelyourself if the trigger is icon-only.