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>
| Prop | Type | Default | Description |
|---|---|---|---|
options | { value: string; label: string }[] | — | Required. Flat list of selectable options. |
value | string | — | Controlled selected value. |
defaultValue | string | — | Uncontrolled initial value. |
onValueChange | (value: string) => void | — | Called when the selection changes. |
placeholder | string | — | Shown in the trigger when nothing is selected. |
disabled | boolean | false | Disables the trigger. |
name | string | — | Form field name, forwarded to the underlying Radix Select. |
className | string | — | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged with .ods-select-trigger. |
children | ReactNode | — | Typically a single <SelectValue>. |
<SelectContent>
| Prop | Type | Default | Description |
|---|---|---|---|
position | "item-aligned" | "popper" | "popper" | Radix positioning strategy. |
sideOffset | number | 4 | Pixel gap between trigger and panel. |
className | string | — | Merged with .ods-select-panel. |
Renders inside a Radix Portal; wraps children in .ods-select-list (the scrollable Viewport, max-height 280px).
<SelectItem>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. |
disabled | boolean | false | — |
children | ReactNode | — | 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/comboboxARIA pattern: arrow keys move the highlighted item,Enter/Spaceselects,Esccloses and returns focus to the trigger, and typeahead jumps to a matching item by typed characters. - The trigger is a real
<button>witharia-expanded/aria-haspopupmanaged 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/SelectTriggeraplaceholderor an associated<label>(e.g. viaField/FieldLabelfrom the Input component set) — an empty trigger with no placeholder has no accessible name.