Select
Picks one option from a list.
Sizes
sm
defaultdefault
lg
Popup
defaultdefault
sm
Native
falsedefault
true
States
rest
placeholder
hover
focus
invalid
disabled
Usage
When to use it
- Use
- One choice from about five to thirty options known up front: a model, a region, a reply language.
- Use
- A form on phones or one built from a schema (native).
- Not for
- Two to five options that fit on screen. Use Radio group.
- Not for
- More than thirty options, or the person types the name. Use Combobox.
- Not for
- The choice switches a view on this page. Use Segmented control.
- Not for
- A list of actions, not a value. Use Dropdown menu.
Rules
9 rules
- Must
- Pass the options as items on Select as well as rendering SelectItems. Because Base UI reads the chosen label from items, so without it the trigger shows the raw value until the popup first opens.
- Must
- Name the trigger with a Field and FieldLabel, or aria-label; its text is the value, not the question. Because a screen reader hears "glaze-large, combobox" with no question.
- Must
- Say what to pick with SelectValue placeholder; never add a None option. Because a None row counts as a real value, so a required field passes empty and the list gains a fake row.
- Must
- Use Combobox past thirty options or for options that load. Because the popup stops at 320px, about nine rows, and has no search or loading state to reach the rest.
- Never
- Draw a ring, a shadow or a black line on SelectTrigger to show focus or the open popup. Because the trigger is a field, and a field shows focus by darkening its own line; a ring makes it read as a button.
- Never
- Square off the trigger or set its height in className; pass size. Because every one-line control is a pill, and a fixed height clips the value at 200% zoom.
- Should
- Pass size="sm" to SelectContent when the trigger is size="sm", so 28px rows match the 26px trigger.
- Should
- Use native for a form used mostly on phones or generated from a schema.
- Should
- Set aria-invalid on the trigger after a submit and say why in the FieldError.
State order
6 states, highest first
- 1
- disabled
- 2
- invalid
- 3
- open
- 4
- focus
- 5
- hover
- 6
- rest
Accessibility
5 checks
- The trigger is a combobox with aria-haspopup="listbox", aria-expanded and aria-invalid; the popup is a listbox of options with aria-selected.
- The highlighted row's wash is the focus indicator inside the popup; forced colors draw a 2px outline on it.
- The chosen row has a check as well as aria-selected, so it never rests on colour.
- Focus darkens the trigger's line to --line-focus (2.3:1 on paper); the caret and the value stay.
- native keeps the platform picker, screen reader and keyboard, and sets 16px text under a coarse pointer.
Keyboard
4 keys
- Enter, Space, Down, Up
- On the trigger: opens with the chosen row highlighted.
- A letter
- Jumps to the next option starting with it, open or closed.
- Enter
- Picks the highlighted row and returns focus to the trigger.
- Escape
- Closes without a change.
Edge cases
7 edge cases
- Long label
- Truncates with an ellipsis in the trigger and the row; the popup stops at --popover wide.
- Forty options
- The list scrolls inside 320px between scroll arrows; use a Combobox.
- Near the viewport bottom
- The popup flips above the trigger and stays 8px inside.
- Right to left
- The check and the caret swap sides.
- Zoom 200%
- The pill grows past its token with the text.
- Inside a Dialog
- The popup portals above it; Escape closes the popup only.
- Disabled Field
- The trigger goes grey with the Field.
API
9 props
Select items{value, label, disabled?}[] | RecordSelect value, defaultValue, onValueChangestring | nullSelect native, size, placeholderboolean, 'sm' | 'default' | 'lg', stringdefaultfalse, 'default'SelectTrigger size'sm' | 'default' | 'lg'default'default'SelectTrigger aria-invalidbooleandefaultfalseSelectValue placeholderReactNodeSelectContent size'default' | 'sm'default'default'SelectContent side, align, alignItemWithTriggerBase UIdefault'bottom', 'start', falseSelectGroup, SelectLabel, SelectSeparatorBase UI
Tokens
25 tokens
--paper- #ffffff
--field- #efefef
--line-control- rgb(5 5 5 / 0.15)
--line-focus- rgb(5 5 5 / 0.32)
--line-faint- rgb(5 5 5 / 0.06)
--line- rgb(5 5 5 / 0.10)
--fail- #e35e49
--flat-hover- #f8f8f8
--hover- rgb(5 5 5 / 0.06)
--text-1- rgb(5 5 5 / 0.80)
--text-3- rgb(5 5 5 / 0.45)
--text-disabled- rgb(5 5 5 / 0.32)
--control-sm- 26px
--control- 32px
--control-lg- 36px
--menu-row- 32px
--menu-row-sm- 28px
--menu-min- 192px
--popup-max- 320px
--popover- 288px
--radius-pill- 999px
--radius-overlay- 10px
--radius-tag- 6px
--type-body-sm- 400 13px/1.5 var(--font-sans)
--type-label- 500 13px/1.35 var(--font-sans)