OperantAI Workflow Design System
ComfortableCompact

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>

PropTypeDefaultDescription
sideOffsetnumber8Pixel 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.
classNamestring—Merged with .ods-popover-panel.
childrenReactNode—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, or PopoverClose) returns focus to the trigger.
  • The trigger is a real <button> — keyboard-focusable and operable via Enter/Space with no extra wiring.
  • Esc closes the popover; clicking outside the panel closes it too.
  • The panel is portalled to the document body, so it escapes any overflow: hidden ancestor and stacks above other content via --z-dropdown.
  • Because PopoverTrigger renders no .ods-* class by default, remember to add aria-label yourself if the trigger is icon-only.