Select Atom
A listbox, not a native <select>: typeahead, roving focus, scroll containment, and form
association come from Base UI, and the popup is a real surface that can be themed and can hold an
indicator per item.
Usage
Payouts convert at the daily rate.
This currency is not supported in your region.
<Field label="Settlement currency" hint="Payouts convert at the daily rate."> <Select items={[{ value: 'idr', label: 'Indonesian rupiah' }, …]} value={value} onValueChange={setValue} /></Field>Inside a Field, the Select picks up the label, the description wiring, and the error state
without being told — the same seam every other control uses.
Choosing between this, a RadioGroup and a AutoComplete
How many options there are decides the control, and the numbers are not a house preference — usability testing puts a dropdown's useful band between roughly five and ten options. Below five, opening a menu to read choices that would have fitted on screen costs a click for nothing. Above ten, the list stops being scannable and typing beats scrolling.
| Options | Reach for | Why |
|---|---|---|
| 2–4 | RadioGroup, variant="segmented" | They all fit at once; a menu hides them behind a press |
| 5–10 | Select | Enough to be worth collapsing, few enough to scan |
| More than ~10 | AutoComplete | Typing is faster than scrolling, and the tail of a long list is never read |
| Too many to ship | AutoComplete with loadItems | The list lives behind a query rather than in the bundle |
Note what the research says to do at the top of that table: replace the control, not add a search box to a dropdown. That is why searching lives in its own component here rather than behind a flag on this one (ADR 0014).
Sources: Baymard, Drop-Down Usability; NN/g, Dropdowns: Design Guidelines; GOV.UK Design System, Select.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
| items | Array<{ value, label, disabled? }> | — | The options. Labels are ReactNode, so an item can carry an icon or a secondary line. |
| value / defaultValue | string | null | — | Controlled or uncontrolled. |
| onValueChange | (value: string) => void | — | Receives the value only — Base UI’s event details do not leak through. |
| placeholder | string | 'Select…' | Shown while nothing is chosen. |
| size | 'sm' | 'md' | 'lg' | 'md' | Matches the control height of Input at the same size. |
| name | string | — | Emits a hidden input, so it posts inside a plain form. |
| disabled / invalid | boolean | — | Inside a Field, invalid is inherited. |
Do
Keep option labels short enough to read in the trigger — the trigger shows the chosen label, not the value.
Don't
Use a Select for two options; that is a segmented RadioGroup or a Switch, and both show their choices without a click.
Keyboard
| Key | Behaviour |
|---|---|
Space / Enter | Opens, and selects the highlighted item |
↑ / ↓ | Moves the highlight |
| Typing | Jumps to the first matching label |
Esc | Closes without changing the value |