Headless / Selection

Select

Select is a single-select opened from a button. It is Combobox with the typeable input swapped for a trigger — pick one option, no filtering.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A compound component built on Combobox. Select.Root is Combobox's Root with the text-input props dropped. Select.Trigger is a button that displays the current value and opens the listbox. Select.List is Combobox's List unchanged.

Select deliberately omits type-ahead filtering. When the option list is long enough that the user needs to type, reach for Combobox; Select is for short, scan-able lists where a button and a listbox are enough.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { Select } from '@usemotif/headless';

Anatomy

example.tsx
<Select.Root options={options} placeholder="Choose one">
<Select.Trigger>
  <button>{selectedLabel ?? 'Choose one'}</button>
</Select.Trigger>
<Select.List
  renderOption={(option, { selected }) => (
    <Row option={option} selected={selected} />
  )}
/>
</Select.Root>

API

Select.Root

Select.Root

stable
function Select.Root<T>(props: SelectRootProps<T>): JSX.Element
optionsreadonly ComboboxOption<T>[]required

The option set — each `{ value, label, disabled? }`.

value / defaultValue / onValueChangeT / T / (next: T | undefined) => void

The selected value, controlled or uncontrolled.

open / defaultOpen / onOpenChangeboolean / boolean / (open) => void

The listbox open state.

placeholderstring

Text to show when no value is selected.

Select.Trigger

A button that opens the listbox. Pass a single element child — Select clones it with aria-haspopup="listbox", aria-expanded, aria-controls, and the open handlers. Arrow Down, Enter, and Space all open the list.

Select.List

Combobox's List, unchanged. See the Combobox.List API.

Accessibility

The trigger carries aria-haspopup="listbox" and aria-expanded; the listbox is role="listbox" of role="option" rows. Arrow keys move the highlight, Enter selects, Escape closes. Give the trigger an accessible name — a visible label, or aria-label if the button shows only the value.

Examples

A timezone select:

example.tsx
<Select.Root options={timezones} value={tz} onValueChange={setTz}>
<Select.Trigger>
  <Button variant="outline">{tzLabel ?? 'Select a timezone'}</Button>
</Select.Trigger>
<Select.List
  style={{ padding: 4, background: 'var(--colors-surface-base)' }}
  renderOption={(opt, { selected }) => (
    <Box p="$2" fontWeight={selected ? '$semibold' : undefined}>
      {opt.label}
    </Box>
  )}
/>
</Select.Root>