Skip to main content

Select

betav0.1.0

Single-value dropdown selector for choosing one option from a predefined list. Compound-component API built on @radix-ui/react-select with integrated label, helper text, and error state. Multi-value selection is permanently out of scope (arbitration Q3) — see the dedicated MultiSelect component (ROADMAP, not yet built).

Variants

Default — label, trigger, helper text
Relative time

Choose a time range

Examples

<Select value={value} onValueChange={setValue}>
  <SelectLabel>Relative time</SelectLabel>
  <SelectTrigger>
    <SelectValue placeholder="Select an option..." />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="days-1">Last 24 hours</SelectItem>
    <SelectItem value="days-7">Last 7 days</SelectItem>
    <SelectItem value="months-1">Last month</SelectItem>
  </SelectContent>
  <SelectHelperText>Choose a time range</SelectHelperText>
</Select>
// clearable — the × appears only once a value is set, and never while disabled
// (RULE-12, same contract as Combobox's own `clearable`)
<Select clearable value={value} onValueChange={setValue}>
  <SelectLabel>Relative time</SelectLabel>
  <SelectTrigger>
    <SelectValue placeholder="Select an option..." />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="days-1">Last 24 hours</SelectItem>
    <SelectItem value="days-7">Last 7 days</SelectItem>
  </SelectContent>
</Select>
// error lives on the Root only (arbitration Q1) — SelectTrigger and
// SelectHelperText need no prop, both read it from context
<Select value={value} onValueChange={setValue} error>
  <SelectLabel required>Relative time</SelectLabel>
  <SelectTrigger>
    <SelectValue placeholder="Select an option..." />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="days-1">Last 24 hours</SelectItem>
  </SelectContent>
  <SelectHelperText>This field is required</SelectHelperText>
</Select>
// SelectGroup + SelectSeparator for categorized options (RULE-08: never
// nest a Select inside another Select's content — use groups instead)
<Select value={value} onValueChange={setValue}>
  <SelectLabel>Entity type</SelectLabel>
  <SelectTrigger>
    <SelectValue placeholder="Select a type..." />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectItem value="report">Report</SelectItem>
      <SelectItem value="malware">Malware</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectItem value="identity">Identity</SelectItem>
      <SelectItem value="location">Location</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>
// No SelectLabel — an accessible name must be supplied directly, or FDS
// warns in development (never in production, never blocks rendering)
<Select value={value} onValueChange={setValue}>
  <SelectTrigger aria-label="Filter by status">
    <SelectValue placeholder="All statuses" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="open">Open</SelectItem>
    <SelectItem value="closed">Closed</SelectItem>
  </SelectContent>
</Select>

Props

PropType
Select.valuestring — Controlled selected value
Select.defaultValuestring — Uncontrolled initial value
Select.onValueChange(value: string) => void — Fires when the selected value changes
Select.openboolean — Controlled open state of the dropdown
Select.defaultOpenboolean — Uncontrolled initial open state (default: false)
Select.onOpenChange(open: boolean) => void — Fires when the dropdown opens or closes
Select.disabledboolean — Disables the trigger and prevents the dropdown from opening
Select.requiredboolean — Marks the underlying native form control as required (HTML5 form semantics only). Independent from SelectLabel.required, which separately controls the `*` indicator — set both explicitly if a field should show the asterisk.
Select.namestring — Name attribute used for native form submission
Select.errorboolean — Single source of truth for the field's error state (arbitration Q1). Propagates via context to SelectTrigger (error border) and SelectHelperText (error text color) — not a prop on any sub-part.
Select.clearableboolean — Adds a clear control to the trigger that empties the selection back to the placeholder (RULE-12). Same prop name and same self-hiding behaviour as Combobox's `clearable`: rendered only when there is something to clear, never while disabled. Unlike ComboboxClear it IS a tab stop, because a Select trigger offers no keyboard path to the same action.
Select.clearLabelstring — Accessible name of the clear control (default: "Clear", iso-Combobox). Pass a field-specific name when several clearable fields share a screen.
SelectTrigger.childrenReactNode — Typically a single SelectValue. Dev-only warning if the field has no SelectLabel and no aria-label/aria-labelledby (arbitration Q5) — advisory only, never blocks rendering.
SelectValue.placeholderReactNode — Shown when no value is selected (Radix Value prop)
SelectContent.position"popper" | "item-aligned" — Positioning strategy (default: "popper")
SelectContent.side"top" | "bottom" — Preferred side when using popper positioning
SelectContent.align"start" | "center" | "end" — Alignment when using popper positioning
SelectItem.valuestring (required) — Value submitted/reported when this item is selected
SelectItem.disabledboolean — Disables this individual item (still visible, not selectable)
SelectLabel.childrenReactNode — Label text. Optional (arbitration Q5) — never mandatory.
SelectLabel.requiredboolean — Shows an inline "*" indicator next to the label text
SelectLabel.hasInfoIconboolean — Shows an info icon next to the label text
SelectHelperText.childrenReactNode — Helper or error text. Same slot switches color/content based on Select's `error` state (arbitration Q1) — no separate `error` prop here.
SelectGroup.childrenReactNode — One or more SelectItem, grouped together (RULE-08)
SelectSeparator (no props)Visual divider between SelectGroups — renders a single hairline, no configurable props beyond standard HTML div attributes

Accessibility

2.1 AAWCAG pass

Contrast: the disabled text on the disabled field fill (--text-input-disabled on --bg-input-disabled) is permanently exempt — WCAG 1.4.3 Note 1 excludes inactive user interface components — so no contrast pair is declared for it, deliberately and not by omission. Measured 4.35:1 dark / 3.69:1 light since the 2026-08-20 export moved both tokens onto the gray ramp. Same non-declaration as Menu's disabled row, which renders the identical two classes. RFC §5.4. item-text-hover/-focus/-selected currently share identical token pairs (Radix's Item primitive sets the same data-highlighted attribute for real mouse hover and keyboard arrow-navigation — no distinction is possible — and Figma's own 'Selected' state is documented as visually identical to 'Focus'; RFC §9 Follow-up #7), declared as distinct ids anyway per instruction to cover each state explicitly rather than assume they'll always stay merged. border-elevation-subtle (SelectSeparator) is a decorative structural divider, not a functional UI component boundary — exempt from WCAG 1.4.11 per SC 1.4.11 Note 1 (same treatment Tabs.meta.ts already documents for its own TabsList bottom border). 1.1.1 Non-text Content — chevron and info icons are aria-hidden decorative accents; the field's accessible name/state never depends on them alone. 1.3.1 Info and Relationships — Radix provides role=combobox/listbox/option and aria-selected/aria-expanded automatically; aria-labelledby (SelectLabel) and aria-describedby (SelectHelperText) are wired by FDS via context, not Radix (RFC §9 Follow-up #1 — Radix does not auto-associate an external label the way it does for a Label nested inside a Group). 1.4.1 Use of Color — error state is conveyed by border color AND helper-text content change (RULE-03/04), never color alone. 2.1.1 Keyboard — full Radix keyboard support: Space/Enter/ArrowDown opens, Arrow keys navigate items, typeahead by first character, Escape closes and returns focus to the trigger. 2.4.6 / 4.1.2 — SelectLabel is optional (arbitration Q5), never made mandatory; a dev-only console.warn (silent in production) fires when SelectTrigger has no SelectLabel and no aria-label/aria-labelledby, so a consumer always gets a signal without the API forcing a label to exist. Multi-value selection is permanently out of scope (arbitration Q3) — see the dedicated MultiSelect component (ROADMAP, not yet built); consumers needing `multiple` today keep their existing MUI/wrapper implementation until it ships (RFC §7 transitional facade).