DropdownMenu wraps @radix-ui/react-dropdown-menu and styles it entirely with .ods-dropdown-* classes — Radix supplies positioning, focus management, roving-tabindex keyboard nav, and typeahead; this package supplies visuals only, sharing the same "floating panel" look (--color-surface-elevated + hairline border + --shadow-md) as Select and .ods-modal.
Installation
HTML — already included if you loaded the 3 stylesheets. See the Installation guide. Note: unlike most components in this system, a dropdown menu's interactive behavior — open/close on click, arrow-key roving focus, typeahead, collision-aware positioning, focus return on close — is genuinely JS. The markup below shows the DOM shape and classes for visual reference; use the React component (or equivalent hand-rolled JS) for real interactivity.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML (static markup shape)
<div class="ods-dropdown-panel" style="min-width: 180px">
<div class="ods-dropdown-list">
<div class="ods-dropdown-label">Run</div>
<div class="ods-dropdown-item">View trace</div>
<div class="ods-dropdown-item">Re-run</div>
<div class="ods-dropdown-separator"></div>
<div class="ods-dropdown-item -danger">Delete run</div>
</div>
</div>
React
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@operant/ui";
export default function Example() {
return (
<DropdownMenu>
{/* No asChild (AGENTS.md #2) — the trigger IS the real <button>. */}
<DropdownMenuTrigger className="ods-btn -secondary">Actions</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Run</DropdownMenuLabel>
<DropdownMenuItem>View trace</DropdownMenuItem>
<DropdownMenuItem>Re-run</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="danger">Delete run</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Examples
Basic menu
<button type="button" class="ods-btn -secondary">Actions</button>
<div class="ods-dropdown-panel">
<div class="ods-dropdown-list">
<div class="ods-dropdown-label">Run</div>
<div class="ods-dropdown-item">View trace</div>
<div class="ods-dropdown-item">Re-run</div>
<div class="ods-dropdown-separator"></div>
<div class="ods-dropdown-item -danger">Delete run</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger className="ods-btn -secondary">Actions</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Run</DropdownMenuLabel>
<DropdownMenuItem>View trace</DropdownMenuItem>
<DropdownMenuItem>Re-run</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="danger">Delete run</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>;
With a submenu
Submenus reuse .ods-dropdown-item for the trigger row and .ods-dropdown-panel/.ods-dropdown-list for the nested panel — the same floating-panel language, nested one level.
<div class="ods-dropdown-panel">
<div class="ods-dropdown-list">
<div class="ods-dropdown-item">Copy link</div>
<div class="ods-dropdown-item">
Export as…
<div class="ods-dropdown-panel">
<div class="ods-dropdown-list">
<div class="ods-dropdown-item">JSON</div>
<div class="ods-dropdown-item">CSV</div>
<div class="ods-dropdown-item">Markdown</div>
</div>
</div>
</div>
<div class="ods-dropdown-separator"></div>
<div class="ods-dropdown-item -danger">Delete run</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger className="ods-btn -secondary">Export</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Export as…</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>JSON</DropdownMenuItem>
<DropdownMenuItem>CSV</DropdownMenuItem>
<DropdownMenuItem>Markdown</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem variant="danger">Delete run</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>;
Disabled item
<div class="ods-dropdown-panel">
<div class="ods-dropdown-list">
<div class="ods-dropdown-item">View trace</div>
<div class="ods-dropdown-item" aria-disabled="true" style="opacity: 0.5; pointer-events: none">
Re-run (locked)
</div>
<div class="ods-dropdown-item -danger">Delete run</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger className="ods-btn -secondary">Actions</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>View trace</DropdownMenuItem>
<DropdownMenuItem disabled>Re-run (locked)</DropdownMenuItem>
<DropdownMenuItem variant="danger">Delete run</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>;
API Reference
Components
| Component | Wraps | Class | Notes |
|---|---|---|---|
DropdownMenu | DropdownMenuPrimitive.Root | — | Pass-through, no styling of its own. |
DropdownMenuTrigger | DropdownMenuPrimitive.Trigger | — | Pass-through, no asChild. Style it directly, e.g. className="ods-btn -secondary". |
DropdownMenuSub | DropdownMenuPrimitive.Sub | — | Grouping primitive for a submenu — no styling of its own. |
DropdownMenuContent | DropdownMenuPrimitive.Content (Portal) | .ods-dropdown-panel wrapping .ods-dropdown-list | sideOffset defaults to 4. |
DropdownMenuItem | DropdownMenuPrimitive.Item | .ods-dropdown-item | See variant prop below. |
DropdownMenuSeparator | DropdownMenuPrimitive.Separator | .ods-dropdown-separator | — |
DropdownMenuLabel | DropdownMenuPrimitive.Label | .ods-dropdown-label | Non-interactive group heading. |
DropdownMenuSubTrigger | DropdownMenuPrimitive.SubTrigger | .ods-dropdown-item (reused) | No dedicated disclosure-caret class; pass one as children if you want a trailing affordance. |
DropdownMenuSubContent | DropdownMenuPrimitive.SubContent (Portal) | .ods-dropdown-panel wrapping .ods-dropdown-list | Same floating-panel language as the top-level menu. |
<DropdownMenuItem> props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "danger" | "default" | "danger" adds -danger for destructive actions. |
disabled | boolean | false | Forwarded to Radix; sets data-disabled, which .ods-dropdown-item[data-disabled] styles at opacity: 0.5; pointer-events: none. |
All other props on every component above are forwarded straight through to the corresponding Radix primitive (onSelect, onOpenChange, open, modal, etc.).
HTML-side modifiers: .ods-dropdown-panel/.ods-dropdown-list (container), .ods-dropdown-item (+ -danger), .ods-dropdown-separator, .ods-dropdown-label. Highlighted/disabled item states are driven by Radix's data-highlighted/data-disabled attributes in the React layer — a static HTML mockup has no equivalent without your own hover/focus JS.
Accessibility
- Radix wires the full
menu/menuitemARIA pattern:aria-haspopup,aria-expandedon the trigger, rovingtabindexand arrow-key navigation within the panel, typeahead search, andEscape-to-close with focus returned to the trigger. - The trigger renders as a real
<button>(noasChild) — do not nest another interactive component (like this package'sButton) inside it; that produces an invalid<button><button>tree. Apply.ods-btn -secondary(or any button classes) directly to theDropdownMenuTriggerviaclassName. - Disabled items get
aria-disabledfrom Radix and are skipped during keyboard navigation, not just visually dimmed. DropdownMenuLabelis a non-interactive heading for a group of items — it doesn't participate in keyboard navigation, matching a native<optgroup>label's role.