Select
NewChoose one value from a list that is too long, or too likely to grow, to show all at once: a country, a stock location, a delivery rate. The trigger is an Input frame; the list is the Menu's sheet, growing out of the trigger.
Playground
Change the props, then open the list: try the arrow keys, Enter, and typing the first letters of a row.
<Select
aria-label="Delivery rate"
placeholder="Choose an option"
options={rates}
/>Basics
The common case needs one tag: pass options and Select renders the trigger and the list. Put it in a Field for a label and a hint, and control it with value and onValueChange. The placeholder shows while nothing is chosen; set value="" to bring it back.
Only orders in this state are listed.
Showing all orders
Sizes
sm 30px, md 36px (default) and lg 44px: the Input and Button heights, so a select shares a line with both. The rows in the list follow the size (30, 34 and 40px).
Rows
A row can carry an icon, a quieter description under its label and a hint at the end, such as a price or a stock count. Only the icon and label come up into the trigger once a row is chosen. A disabled row dims and the arrow keys skip it: say why in its hint.
| Part | In the list | In the trigger |
|---|---|---|
| label | 14px at 450, one line, cut short with an ellipsis | Yes |
| icon | 16px in slate, firming to ink on the highlighted row | Yes |
| description | 13px in quiet slate, wrapping under the label | No |
| hint | 13px in quiet slate, at the end of the row | No |
| Check | Teal, on the chosen row only | No |
Groups and separators
For headings and hairlines, compose the parts: a SelectTrigger, then a SelectContent holding SelectItems, with SelectGroup, SelectLabel and SelectSeparator between them. Composed by hand, the trigger takes the same props as Select does in the simple form.
With options, a group is an entry with a label and its own options, and hairlines are drawn between groups for you.
Long lists
The list stops at 320px, or where the window runs out, and scrolls. Soft arrows at either end show there is more, and resting the pointer on one scrolls the list. With the list closed or open, typing the first letters of a row jumps to it: try "sw" for Sweden.
Type the first letters of a name to jump to it.
States
The trigger is an Input in every state. Hover firms the hairline; keyboard focus, and the whole time the list is open, turn it teal with the soft halo. An error in the Field swaps the teal for red. Disabled sinks into the well. A long value is cut short with an ellipsis rather than pushing the chevron out.
Choose a delivery rate for this zone
Set by the order's sales channel
A leading icon
icon on the select puts a fixed icon at the start of the trigger, muted until it has focus, as on an Input. Leave it out when the rows have icons of their own: the chosen row's icon shows there instead.
In a dialog
The list is portalled and sits above dialogs and sheets, so it opens over the dialog's edge rather than being cut off by it, and Escape closes the list before the dialog. Press Transfer without choosing where the stock is going to see the error.
In both themes
Held open for review: delivery rates with descriptions and prices, and a grouped list long enough to scroll, light above dark. The sheet is the canvas colour in light and lifts to graphite in dark, as the Menu does.
Open them for real
Each select opens its list in the theme of its own side.
Guidelines
- Order rows the way people look for them: alphabetically for names and places, by size or speed for rates, most used first only when the list is short.
- Keep labels short and distinct from their first words, so type-to-select finds them.
- Never use a row as a fake placeholder ("Please choose"). Use placeholder, and value="" to clear.
- If choosing should do something straight away (sort a list, switch a view), that is fine; if it starts an action, use a Menu.
| To choose | Use |
|---|---|
| One of many, or a list that grows (countries, locations, staff) | Select |
| One of two to five, with descriptions worth reading side by side | Radio group |
| One of a few views or ranges, applied at once | Segmented control |
| An action to run, not a value to keep | Menu |
| One of hundreds, or by searching | Command palette |
Accessibility
- Built on Radix Select: the trigger is a combobox button and the list a listbox. Space, Enter and the arrow keys open it; the arrow keys, Home and End move; Enter or Space chooses; Escape closes; typing jumps to a row.
- In a Field, the label names the trigger and the hint or error is its description. An error also sets
aria-invalid. Outside a Field, give it anaria-label. - A row's description and hint are read after its label, as its description.
- A hidden native select goes with it, so
nameandrequiredwork in forms, and resetting the form brings back the starting value. - Focus returns to the trigger when the list closes. Disabled rows are skipped.
- The list follows the nearest ThemeScope, though it is portalled out of it.
- Under reduced motion the list fades in and out without growing, and its rows fade without rising.
Props
Select
| Prop | Type | Default | Description |
|---|---|---|---|
options | Array<SelectOption | SelectOptionGroup> | None | The rows, and groups of rows. With it, Select renders the trigger and the list; without it, compose the parts as children. |
value | string | '' | None | Controlled value. '' clears it and the placeholder shows again. |
defaultValue | string | None | Starting value when uncontrolled. |
onValueChange | (value: string) => void | None | Called with the chosen value. Typed options keep it typed. |
placeholder | ReactNode | None | With options: shown in quiet slate while nothing is chosen. |
size | 'sm' | 'md' | 'lg' | 'md' | Trigger height (30, 36 or 44px) and row height (30, 34 or 40px). |
disabled | boolean | false | Cannot be opened; the trigger sinks into the well. |
name, required, form | string, boolean, string | None | For the hidden native select that forms submit. |
open, defaultOpen, onOpenChange | boolean, boolean, (open) => void | None | Control the list being open. |
contentWidth | number | string | 'trigger' | None | With options: the width of the list. By default at least the trigger. |
With options, every other prop (id, aria-label, icon, invalid, className) and the ref go to the trigger.
SelectOption
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | string | None | Unique, and never an empty string. |
labelRequired | ReactNode | None | The row, and the trigger once chosen. |
description | ReactNode | None | A quieter second line, in the list only. |
icon | ReactNode | None | Leading 16px icon, shown in the trigger too. |
hint | ReactNode | None | Quiet trailing text, e.g. a price or a stock count. |
disabled | boolean | None | Dimmed and skipped by the arrow keys. |
textValue | string | None | Text for type-to-select when the label is not plain text. |
SelectTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | ReactNode | None | Shown while nothing is chosen, when the trigger has no SelectValue of its own. |
icon | ReactNode | None | A fixed leading icon, muted until focus. |
invalid | boolean | None | Red edge and aria-invalid. In a Field, follows its error; aria-invalid works too. |
size | 'sm' | 'md' | 'lg' | None | Overrides the size set on Select. |
children | ReactNode | None | Defaults to a SelectValue. Pass your own to show something else. |
SelectContent
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | string | 'trigger' | None | Width of the list. By default at least the trigger, up to 360px for longer rows. |
position | 'popper' | 'item-aligned' | 'popper' | Below the trigger, or over it with the chosen row lined up (macOS style). |
side, align, sideOffset | Radix placement | 'bottom', 'start', 6 | Where the list opens. It flips when there is no room. |
onScroll | (event) => void | None | The list's own scrolling, e.g. to load more rows near the end. |
container | HTMLElement | null | None | Where it is portalled. Defaults to document.body. |
SelectItem
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | string | None | Unique, and never an empty string. |
childrenRequired | ReactNode | None | The label, mirrored into the trigger once chosen. |
description | ReactNode | None | A quieter second line, in the list only. |
icon | ReactNode | None | Leading 16px icon, mirrored into the trigger. |
hint | ReactNode | None | Quiet trailing text. |
disabled | boolean | None | Dimmed and skipped. |
textValue | string | None | Text for type-to-select when children is not plain text. |
SelectValue takes a placeholder. SelectGroup, SelectLabel (inside a group) and SelectSeparator take no props of their own.