OperantAI Workflow Design System
ComfortableCompact

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

ComponentWrapsClassNotes
DropdownMenuDropdownMenuPrimitive.Root—Pass-through, no styling of its own.
DropdownMenuTriggerDropdownMenuPrimitive.Trigger—Pass-through, no asChild. Style it directly, e.g. className="ods-btn -secondary".
DropdownMenuSubDropdownMenuPrimitive.Sub—Grouping primitive for a submenu — no styling of its own.
DropdownMenuContentDropdownMenuPrimitive.Content (Portal).ods-dropdown-panel wrapping .ods-dropdown-listsideOffset defaults to 4.
DropdownMenuItemDropdownMenuPrimitive.Item.ods-dropdown-itemSee variant prop below.
DropdownMenuSeparatorDropdownMenuPrimitive.Separator.ods-dropdown-separator—
DropdownMenuLabelDropdownMenuPrimitive.Label.ods-dropdown-labelNon-interactive group heading.
DropdownMenuSubTriggerDropdownMenuPrimitive.SubTrigger.ods-dropdown-item (reused)No dedicated disclosure-caret class; pass one as children if you want a trailing affordance.
DropdownMenuSubContentDropdownMenuPrimitive.SubContent (Portal).ods-dropdown-panel wrapping .ods-dropdown-listSame floating-panel language as the top-level menu.

<DropdownMenuItem> props

PropTypeDefaultDescription
variant"default" | "danger""default""danger" adds -danger for destructive actions.
disabledbooleanfalseForwarded 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/menuitem ARIA pattern: aria-haspopup, aria-expanded on the trigger, roving tabindex and arrow-key navigation within the panel, typeahead search, and Escape-to-close with focus returned to the trigger.
  • The trigger renders as a real <button> (no asChild) — do not nest another interactive component (like this package's Button) inside it; that produces an invalid <button><button> tree. Apply .ods-btn -secondary (or any button classes) directly to the DropdownMenuTrigger via className.
  • Disabled items get aria-disabled from Radix and are skipped during keyboard navigation, not just visually dimmed.
  • DropdownMenuLabel is a non-interactive heading for a group of items — it doesn't participate in keyboard navigation, matching a native <optgroup> label's role.