Select
Picks one option from a list in a popup.
Examples
Pick the language replies are written in.
Replies use this unless the customer writes in another.
Sizes
defaultdefault
sm
lg
States
rest
hover
focus-visible
placeholder
invalid
disabled
Usage
When to use it
- Use
- One choice from 5 to about 30 known options.
- Use
- Options of different kinds share names: pass kind.
- Use
- A form used mostly on phones or generated from a schema: native.
- Not for
- Two to six options fit on screen. Use Radio group.
- Not for
- There are more than 30 options, or the person types the name. Use Combobox.
- Not for
- The choice switches a view on this page. Use Segmented control.
Rules
9 rules
- Must
- Pass the options as items on Select as well as rendering SelectItems, so the trigger shows the label before the popup opens. Because Base UI finds the label from items, so without it the trigger shows the raw value "agent:triage" until the popup opens.
- Must
- Label the trigger with Field and FieldLabel, or aria-labelledby; its text is the value, not the question. Because the trigger text is the current value, so a screen reader hears "English, combobox" with no question.
- Must
- Use SelectValue placeholder to say what to pick; never add a fake first option like None. Because a None option counts as a real choice, so a required field passes empty and the list gains a fake row.
- Must
- Pass kind on options that share a name; never mark kinds with a colored dot. Because a dot is hue alone, which vanishes in forced colors and for color-blind readers; kind adds the word.
- Should
- Use native for forms used mostly on phones or generated from a schema.
- Must
- Use Combobox past 30 options; the popup caps at 320px, about nine items. Because the popup caps at 320px, about nine items, and Select has no search to reach the rest.
- Must
- Pass only options known up front; options that load belong in Combobox. Because Select has no loading or empty state, so options that arrive late leave a popup that opens blank.
- Never
- Render a Select with no options; show why in its place. Because an empty popup opens to nothing and gives no reason or way forward.
- Should
- Set aria-invalid on the trigger and show the error in the Field.
Accessibility
6 checks
- The trigger is a combobox with aria-expanded, aria-haspopup="listbox" and aria-invalid.
- The popup is a listbox; each item is an option with aria-selected.
- Enter, Space or ArrowDown on the trigger opens it with the chosen item highlighted.
- Letters jump to the next item starting with them, open or closed.
- Enter picks and returns focus to the trigger; Escape closes without a change.
- The kind label is text after the option name, so it reads without colour.
Edge cases
9 edge cases
- Long label
- Value and item text truncate with an ellipsis; the popup is at most --popover wide.
- 40 options at the viewport edge
- The list scrolls inside a 320px popup and flips above the trigger near the bottom.
- Right to left
- The check and the caret sit at the end, the left in RTL.
- Zoom 200%
- Height is max(--control, 1lh + 12px) and grows with the text.
- Invalid with no value
- The edge takes the fail line; the Field shows the error.
- items=[]
- Opens an empty popup. Do not render it.
- Inside a Dialog
- The popup portals above the dialog; Escape closes only the popup.
- Native on touch
- Opens the platform picker; text is 16px so iOS does not zoom.
- Typing "sp" fast while closed
- Typeahead jumps to Spanish without opening.
API
9 props
Select items{value, label, kind?, disabled?}[] | RecordSelect value / defaultValue / onValueChangestring | nullSelect nativebooleandefaultfalseSelect size, placeholderControlSize; stringdefault'default'SelectTrigger size'sm' | 'default' | 'lg'default'default'SelectTrigger aria-invalidbooleandefaultfalseSelectValue placeholderReactNodeSelectItem kind'agent' | 'key' | 'run' | 'deploy' | 'model'defaultfrom itemsSelectContent side, align, sideOffsetBase UIdefault'bottom', 'start', 4
Tokens
23 tokens
--line-control- oklch(var(--ink-c) / 0.16)
--control- 32px
--control-sm- 28px
--radius-inner- 4px
--radius-soft- 6px
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--accent-wash- var(--surface-selected)
--accent-mark- var(--ink)
--surface-pressed- oklch(var(--ink-c) / 0.08)
--text-disabled- oklch(var(--ink-c) / 0.40)
--surface-page- var(--paper)
--focus-halo- 0 0 0 3px oklch(var(--ink-c) / 0.08)
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--dur-base- 160ms
--dur-fast- 120ms
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--surface-hover- oklch(var(--ink-c) / 0.05)
--type-label- 400 14px/16px var(--font-sans)
--accent-tint- var(--surface-selected)
--accent-label- var(--text-1)
--fail-line- oklch(60% 0.236 26)
--popup-max- 320px
--popover- 288px