OperantAI Workflow Design System
ComfortableCompact

Select is a single-choice dropdown that opens a floating, portalled panel — it shares the same visual language ("FLOATING SURFACES" in css/components.css) as Popover, DropdownMenu, and Tooltip. Most consumers should reach for SimpleSelect, a convenience component that wires the primitives together from a flat list of { value, label } options; use the lower-level Select/SelectTrigger/SelectContent/SelectItem primitives directly only when you need custom item content or grouping.

Installation

HTML — already included if you loaded the 3 stylesheets — 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's plain-HTML select uses the shared .ods-select class from the "INPUTS / TEXTAREA / SELECT" section — a plain, unstyled-panel native <select> (no floating panel, no checkmark items; those require Radix's positioning/portal logic). For the full floating-panel treatment shown in Examples below, use the React version.

<select class="ods-select">
  <option value="" disabled selected>Filter by status</option>
  <option value="queued">Queued</option>
  <option value="running">Running</option>
  <option value="review">Needs review</option>
  <option value="complete">Complete</option>
  <option value="failed">Failed</option>
</select>

React

import { SimpleSelect } from "@operant/ui";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState<string>();
  return (
    <SimpleSelect
      value={value}
      onValueChange={setValue}
      placeholder="Filter by status"
      options={[
        { value: "queued", label: "Queued" },
        { value: "running", label: "Running" },
        { value: "review", label: "Needs review" },
        { value: "complete", label: "Complete" },
        { value: "failed", label: "Failed" },
      ]}
    />
  );
}

Examples

Basic

SimpleSelect takes a flat options array and wires up Select + SelectTrigger + SelectValue + SelectContent + SelectItem for you.

HTML

<select class="ods-select">
  <option value="" disabled selected>Filter by status</option>
  <option value="queued">Queued</option>
  <option value="running">Running</option>
  <option value="review">Needs review</option>
  <option value="complete">Complete</option>
  <option value="failed">Failed</option>
</select>

React

const [value, setValue] = useState<string>();
<SimpleSelect
  options={runOptions}
  value={value}
  onValueChange={setValue}
  placeholder="Filter by status"
/>

With a default value

Uncontrolled usage via defaultValue — the trigger shows the matching option's label immediately, no controlled state required.

HTML

<select class="ods-select">
  <option value="queued">Queued</option>
  <option value="running" selected>Running</option>
  <option value="review">Needs review</option>
</select>

React

<SimpleSelect options={runOptions} defaultValue="running" placeholder="Filter by status" />

Disabled

.ods-select-trigger:disabled / .is-disabled drops opacity to 50% and removes pointer events.

HTML

<select class="ods-select" disabled>
  <option value="queued" selected>Queued</option>
</select>

React

<SimpleSelect options={runOptions} defaultValue="queued" disabled placeholder="Filter by status" />

Composed (manual primitives)

For cases SimpleSelect doesn't cover — custom item content, icons inside items, grouping — compose the primitives directly.

React

import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@operant/ui";
import { useState } from "react";

export default function Composed() {
  const [value, setValue] = useState<string>();
  return (
    <Select value={value} onValueChange={setValue}>
      <SelectTrigger style={{ width: 220 }}>
        <SelectValue placeholder="Choose an environment" />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="dev">Development</SelectItem>
        <SelectItem value="staging">Staging</SelectItem>
        <SelectItem value="prod">Production</SelectItem>
      </SelectContent>
    </Select>
  );
}

There is no equivalent plain-HTML composition beyond the native <select> shown above — the floating panel with per-item checkmarks is a Radix/React-only affordance.

API Reference

<SimpleSelect>

PropTypeDefaultDescription
options{ value: string; label: string }[]—Required. Flat list of selectable options.
valuestring—Controlled selected value.
defaultValuestring—Uncontrolled initial value.
onValueChange(value: string) => void—Called when the selection changes.
placeholderstring—Shown in the trigger when nothing is selected.
disabledbooleanfalseDisables the trigger.
namestring—Form field name, forwarded to the underlying Radix Select.
classNamestring—Merged onto the trigger (.ods-select-trigger).

<Select> / <SelectValue>

Direct re-exports of Radix SelectPrimitive.Root / .Value — no added styling. Select owns open/selected state; SelectValue renders the current selection (or placeholder) inside the trigger.

<SelectTrigger>

Renders .ods-select-trigger — a real <button> — with a trailing .ods-select-caret chevron icon appended automatically after children.

PropTypeDefaultDescription
classNamestring—Merged with .ods-select-trigger.
childrenReactNode—Typically a single <SelectValue>.

<SelectContent>

PropTypeDefaultDescription
position"item-aligned" | "popper""popper"Radix positioning strategy.
sideOffsetnumber4Pixel gap between trigger and panel.
classNamestring—Merged with .ods-select-panel.

Renders inside a Radix Portal; wraps children in .ods-select-list (the scrollable Viewport, max-height 280px).

<SelectItem>

PropTypeDefaultDescription
valuestring—Required.
disabledbooleanfalse—
childrenReactNode—Item label text, wrapped in SelectPrimitive.ItemText.

Renders .ods-select-item with a trailing checkmark (SelectPrimitive.ItemIndicator) shown only when [data-state="checked"].

HTML-side classes: .ods-select (plain native <select>, shared styling with .ods-input/.ods-textarea), .ods-select-trigger (the React floating-panel trigger button; [data-placeholder] dims to --color-text-faint, [data-state="open"]/:focus-visible apply --shadow-focus), .ods-select-panel (elevated surface + border + shadow-md), .ods-select-list (scrollable viewport), .ods-select-item ([data-highlighted] → --color-surface-selected background; [data-disabled] → 50% opacity), .ods-select-caret.

Accessibility

  • Radix Select implements the full listbox/combobox ARIA pattern: arrow keys move the highlighted item, Enter/Space selects, Esc closes and returns focus to the trigger, and typeahead jumps to a matching item by typed characters.
  • The trigger is a real <button> with aria-expanded/aria-haspopup managed automatically by Radix.
  • The panel is portalled to the document body so it escapes clipping ancestors and stacks via --z-dropdown.
  • Disabled items ([data-disabled]) are skipped during keyboard navigation, not just visually dimmed.
  • Always give SimpleSelect/SelectTrigger a placeholder or an associated <label> (e.g. via Field/FieldLabel from the Input component set) — an empty trigger with no placeholder has no accessible name.