Select

Picks one value from a list that doesn't need to stay on screen — a service, a staff member, a duration. A squircle field the height of a Button, over the vendored Base UI select. On a phone the list opens as a bottom sheet, as every dialog does there: tall rows, the page dimmed, a handle to drag it down by.

Playground

Select
State

Sizes

sm 32px, md 36px by default, lg 40px — the same heights as Button and Input, so a select and the action beside it sit on one line.

Labels, not ids

The trigger shows the chosen value as it is. When values are ids, pass items — the same list you render — so it shows “Maja”, not stf_maja.

Groups

Past half a dozen options, group them under a SelectLabel and part the groups with a SelectSeparator.

In a form

Give the Select an id — it lands on the trigger — and point the FieldLabel's htmlFor at it. Stretch the trigger with className="w-full".

Disabled & invalid

Disable the whole Select, or single items. Mark the trigger aria-invalid for a destructive edge, and say what to do in the text below it.

API reference

Select

PropTypeDefaultDescription
valueValue | null—Controlled value; null shows the placeholder. Pair with onValueChange.
defaultValueValue | null—Initial value when uncontrolled.
onValueChange(value: Value | null, details) => void—Called with the chosen item's value.
items{ value, label }[] | Record<string, ReactNode>—The options' labels, so the trigger shows a label rather than the raw value. Pass it whenever values are ids.
idstring—Lands on the trigger, for a FieldLabel's htmlFor.
disabledbooleanfalseDims the trigger and ignores presses.
namestring—Submits the value with a form, via a hidden input.

SelectTrigger

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"32, 36 or 40px tall — Button's heights; 40 at sm and md on a touch screen.
aria-invalidboolean—A destructive edge; describe the fix nearby.
aria-labelstring—The name when no FieldLabel points at it.
classNamestring—Layout only — w-full, a fixed width. Never restyle the field here.

SelectValue, SelectContent, SelectItem

PropTypeDefaultDescription
SelectValue placeholderReactNode—Muted text in the trigger while nothing is chosen.
SelectContent alignItemWithTriggerbooleantrueOpens the list over the trigger with the chosen item aligned to it; false drops it below. Always off for touch.
SelectContent side / align"top" | "bottom" … / "start" | "center" | "end""bottom" / "center"Placement when not aligned to the trigger.
SelectItem valueValue—What the Select's value becomes. Children are the label in the list.
SelectItem disabledbooleanfalseShown, dimmed, and skipped by the keyboard.

SelectGroup wraps related items under a SelectLabel; SelectSeparator draws a hairline between groups. Everything else passes through to the Base UI select parts.

Guidelines

  • Reach for a Select at roughly five to fifteen options. Two to four that deserve to be seen side by side belong in a RadioGroup; a long, searchable list (clients) needs a combobox instead.
  • Pass items whenever values are ids — without it the trigger shows the raw value.
  • Every select has a name: a FieldLabel pointing at its id, or an aria-label on the trigger. The placeholder is not a name.
  • A placeholder prompts (“Pick a service”); it never stands in for a real choice. When “anyone” is a valid answer, make it an item — “Anyone available” — and preselect it.
  • Options are short, in sentence case, the distinguishing word first — “Skin fade · 45 min”, not “45 min · Skin fade” — so typing a letter jumps to the right one.
  • Match sizes with neighbours: a select beside a sm button is sm too.