Select
Single-choice menu on the field surface. Trigger matches Input.
Select picks one value from a short list. Its trigger matches
Input in height, surface, border, and focus ring, so the two
line up in a form, and the list opens below it on bg-surface-raised with a check beside
the chosen option. It is built on Radix Select, and inside a <form> its value submits like
a native select's. When the list is long enough that people would rather type to find an
option, use Combobox.
Usage
import { Field, FieldLabel } from '@/components/ui/field';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
<Field>
<FieldLabel htmlFor="status">Status</FieldLabel>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger id="status">
<SelectValue placeholder="Pick a status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</Field>The file exports Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
SelectGroup, SelectLabel, SelectSeparator, SelectScrollUpButton, and
SelectScrollDownButton. Values are strings. SelectValue shows the chosen option's text in
the trigger, and its placeholder shows in muted text while there is no value, so setting
value to "" clears the select back to the placeholder.
Label it from a Field
Give the trigger an id and point a FieldLabel's htmlFor at it, as above. The trigger is a
native button, so the label names it, and the chosen option is read as its value. Without a
visible label, set aria-label on the trigger.
Set aria-invalid on the trigger when the value fails validation, and the border turns
destructive, the same as Input and Textarea. Say why in a
FieldError and point the trigger's aria-describedby at it.
FieldError is an alert, so it is announced when it appears. Pick a priority below to clear
the error.
Pick a priority before you save.
Groups, separators, and disabled options
Wrap options in a SelectGroup with a SelectLabel first, and the label becomes the group's
name, set in semibold. SelectSeparator draws a 1px rule between groups, and the demo at the
top of the page uses both.
A disabled option stays in the list at 64% opacity, ignores the pointer, and is skipped by
the arrow keys and by typing. disabled on Select disables the trigger, which drops to 64%
opacity with a not-allowed cursor, and inside a Field the label dims with it.
Where the list opens
The list opens 4px below the trigger, at least as wide as it and as tall as the space on screen allows, and when the options run past that, caret buttons at the top and bottom scroll it. It fades in and out, and it closes when the window loses focus or is resized.
SelectContent takes Radix's positioning props, like side, align, and sideOffset. It
defaults position to "popper", which is what places the list below the trigger. Set
position="item-aligned" to lay the list over the trigger instead, with the chosen option
sitting on top of it.
In a form
Inside a <form>, or with form set to a form's id, Select renders a hidden native
<select> beside the trigger and keeps it in step, so the value submits under name like
any other field. required and disabled carry over to it, and resetting the form puts back
the value the Select started with.
<form action={saveIssue}>
<Select name="priority" defaultValue="medium" required>
<SelectTrigger aria-label="Priority">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
</form>Accessibility
- The trigger is a native
<button>with thecomboboxrole andaria-expanded. Its name comes from its FieldLabel oraria-label, and the chosen option is its value, so the demo at the top reads as "Fruit" with the value "Apple".requiredsetsaria-required, andaria-invalidreports the field as invalid. - Enter, Space, ↑, or ↓ opens the list and focuses the chosen option, or the first option when nothing is chosen. Typing a letter on the closed trigger picks the next option that starts with it, without opening the list.
- In the list, ↑ and ↓ move between options and stop at the ends, Home and End jump to the first and last, and typing moves to the next option that starts with what was typed. Enter or Space picks the focused option and closes the list, and Escape or a click outside closes it without a change. Either way, focus returns to the trigger. Tab does nothing while the list is open.
- The list is a
listboxofoptionrows, each named by its text, and the chosen option is announced as selected when focus reaches it. A group is agroupnamed by itsSelectLabel. The check, the separators, and the scroll buttons are hidden from assistive tech. - While the list is open, the rest of the page is hidden from assistive tech and does not scroll, and the pointer cannot reach anything outside the list, so a click outside only closes it.
- Focus shows a 2px ring on the trigger on
:focus-visibleonly, and in forced colors the trigger keeps a transparent outline that the system paints, so focus stays visible in Windows high contrast. - The focused option is marked only by a background wash, which forced colors repaints, so in Windows high contrast it has no mark of its own.
- The trigger is 40px tall and options are 32px, or 36px from the
mdbreakpoint up, and neither grows on a touch screen. - Under reduced motion the list opens and closes without its fade.
When to use something else
- Combobox when the list is long enough to search, like people or projects, or when the field holds several values. It filters as you type, and it adds nothing to a form, so you read its value from your own state.
- Radio group for a few options that are worth showing all at once.
- Dropdown menu for actions on a thing, rather than a value for a field.
Install
shadcn add @gesso/select