Select
Preview appears after the next Storybook update.
<Select />When to use
Use it for
- One choice from about 4 to 10 known options: a theme, a brand, a token group, a role.
- The options are long words or labels that would crowd a RadioGroup.
- Space is tight: in a dense toolbar, a menu or a settings row (size sm). In a page's filter bar, size md, like the search field beside it.
- Each option carries a short fact worth seeing before the pick, like a status or a count: put a Badge in badge. It shows on the item and in the trigger once selected.
Not for
- Two or three options that should all stay visible: use RadioGroup, or an attached ButtonGroup for a view switch.
- Long or unknown lists (people, repositories, 20+ tokens): use Combobox so users can type to filter.
- Several values at once: use Combobox with multiple, or a CheckboxGroup.
- Actions (Duplicate, Delete): use Menu. A Select changes a value, it does not run commands.
Do and don't
Every example is rendered with the real component, so it always matches what ships.
A status next to the label with badge
A Badge in the option's badge slot says the status at a glance, in the list and in the trigger. The description stays for a second line of detail, like who made it.
Order options by meaning, not alphabetically by default
Put the most common choice first, or follow the natural order (System, Light, Dark). Group long lists under short headings.
Explain a disabled option
A description on the option says why it cannot be picked, instead of leaving users guessing.
A Select with two options
Hiding two choices behind a click costs a step for nothing. Show them as radios or a Switch.
The placeholder as an instruction
"Click to select" says nothing. The placeholder describes the empty state: "No theme", "Any reviewer".
Accessibility
- Base UI Select: the trigger is a combobox button with aria-expanded; the popup is a listbox with options marked aria-selected and aria-disabled.
- Keyboard: Enter, Space or Arrow keys open it; arrows move; typing jumps to a matching option; Enter selects; Escape closes and returns focus to the trigger.
- The Field label names the trigger; hint and error are linked through aria-describedby, and an error sets aria-invalid.
- The selected option shows a check mark and a medium weight, not color alone.
- The trigger edge uses color.border.strong (3:1 against surfaces) and the signal focus ring on keyboard focus.
- The popup is portaled and re-applies the theme, brand and density of its trigger.
Content guidelines
- Label is a noun in sentence case: "Theme", "Brand", "Reviewer role".
- Options are short, parallel nouns, sentence case, no trailing punctuation.
- Group headings are one or two words and never repeat the label.
- Placeholders describe the empty value ("No theme"), not the action.
From the code Generated from the component, not editable here.
Anatomy and tokens
Read from packages/react/src/components/Select/Select.module.css at build time. Every value is a token: open one to see its alias chain in both themes.
- Trigger box (same edge and focus as Input)
.trigger - sm
.sm - lg
.lg - Trigger box (same edge and focus as Input), hover
@media (hover: hover) .trigger:hover:not([data-disabled], [aria-invalid="true"], [data-invalid]) - Trigger box (same edge and focus as Input), focus
.trigger:focus-visible, .trigger[data-popup-open] - Trigger box (same edge and focus as Input)
.trigger[data-invalid], .trigger[aria-invalid="true"] - Trigger box (same edge and focus as Input), focus
.trigger[data-invalid]:focus-visible, .trigger[aria-invalid="true"]:focus-visible - Trigger box (same edge and focus as Input)
.trigger[data-disabled] - Trigger box (same edge and focus as Input)
.trigger[data-readonly] - valueWithBadge
.valueWithBadge - Selected value or placeholder
.value[data-placeholder] - valueWithBadge
.valueWithBadge - Selected value or placeholder
.value[data-placeholder] - Prefix icon
.affix - Chevron, turns when open
.chevron - positioner
.positioner - Popup surface (popup.* tokens)
.popup - Popup surface (popup.* tokens)
.popup[data-ending-style] - Scrollable list
.list - Option
.item - Option
.item[data-highlighted] - Option
.item[data-selected] .itemText - Option
.item[data-disabled] - Option description
.itemDescription - Option
.item[data-disabled] .itemDescription - Check mark on the selected option
.indicator - Group heading
.groupLabel - Divider between groups
.separator - Trigger box (same edge and focus as Input)
@media (pointer: coarse) .trigger
Motion and interaction
Select animates on hover, open and close. Try it in the preview at the top.
| Part | When | What changes | Duration | Easing |
|---|---|---|---|---|
| Trigger | Hover | Border | motion.duration.instant75 ms | motion.easing.standardsettles in place |
| Trigger | Open and close | Border, shadow and movement | motion.duration.fast150 ms | motion.easing.standardsettles in place |
| Popup | Open | Fade and movement | motion.duration.fast150 ms | motion.easing.enterstarts fast, lands softly |
| Popup | Close | Fade and movement | motion.duration.fast150 ms | motion.easing.exitstarts softly, leaves fast |
With reduced motion turned on, every change happens at once.
From the code Generated from the component, not editable here.
Props
Parsed from the TypeScript types in packages/react/src/components/Select/Select.tsx, so this table cannot drift from the code. It also accepts every native <button> attribute except value, defaultValue, onChange, prefix, children.
| Prop | Type | Default | Description |
|---|---|---|---|
optionsrequired | SelectOption[] | none | The choices. Keep them under about ten; use Combobox for long or searchable lists. |
placeholder | string | 'Select an option' | Text shown when nothing is selected. Describe the choice, not the action: "No theme". |
value | string | null | none | Selected value (controlled). `null` means nothing selected. |
defaultValue | string | null | none | Initially selected value (uncontrolled). |
onValueChange | (value: string | null) => void | none | Called with the new value when the user picks an option. |
label | string | none | Visible label. When set, the Select wraps itself in a Field. Leave unset inside a Field. |
hint | ReactNode | none | Help under the label (Field description). Only with `label`. |
error | ReactNode | none | Error message. Marks the select invalid and announces the message. Only with `label`. |
hideLabel | boolean | none | Visually hide the Field label (it stays the accessible name). |
required | boolean | none | No description in the code yet. |
name | string | none | Name for form submission (a hidden input carries the value). |
size | 'sm' | 'md' | 'lg' | 'md' | Height follows the density tokens. Default `md`. |
prefix | ReactNode | none | Content before the value, usually an Icon. |
open | boolean | none | Popup open state (controlled). |
defaultOpen | boolean | none | Open on first render (uncontrolled). |
onOpenChange | (open: boolean) => void | none | No description in the code yet. |
portalContainer | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | none | Where the popup is rendered. Default: the end of the body, above every layer including a Dialog or Drawer, so the list is never covered. Pass an element (or a ref) to keep it inside a container, for example a shadow root. |
Playground
Change the props and see the real Select update. Controls are generated from the TypeScript props; nothing here changes the system.
<Select
placeholder="No theme"
label="Theme"
defaultValue="dark"
/>Workbench
For contributors: every state with the accessibility panel, in the component workbench (Storybook). The stories double as visual test fixtures; the guidance on this page is the user documentation (open Select in Storybook).
Embed Storybook stories here (7)
Loads from https://bridge-storybook.floriangouloubi.com (set STORYBOOK_URL at build time). If Storybook is not running, the frames stay empty.
Default
WithValue
WithError
Grouped
WithBadges
Open
AllStates
From the code Generated from the component, not editable here.
Changelog
- New
The measure now writes the Figma sets drawn under a component's folder as their own entries, so Claude can fix them: popups drawn open (Dialog/Popup, AlertDialog/Popup, Drawer/Popup, Menu/Popup, Popover/Popup, Tooltip/Popup, and Select/Popup or Combobox/Popup, measured open when the stories show them closed) and sets that fix a prop (Pagination/Full, Pagination/Compact, Progress/Linear)
v1.0.0, Nov 11, 2026
- New
Push works like publishing a library in Figma: a Changes list grouped Variables and Components with a checkbox per item (all checked by default, select all per group), a Description that opens the pull request and every request, and one "Push for review"
v1.0.0, Nov 11, 2026
- Fix
Component specs treat a button or a select (including the Base UI part that renders one, read from Base UI's docs) as border-box: NumberField steppers are their height token wide again
v1.0.0, Nov 11, 2026
- Fix
Component specs count height and width as the content box (a bordered table cell is its height plus its border), the Figma build draws an empty sized slot at its padding instead of 100px, and stories reach every Figma variant: a disabled control on IconButton, Input, Select, Combobox and Textarea, and Figma variant stories for AccordionItem, MenuItem, MenuCheckboxItem, MenuSubmenu and TableEmpty
v1.0.0, Nov 11, 2026
- Fix
Select and Combobox: the highlighted option's corners now follow the popup's corners, like Menu items
v1.0.0, Nov 11, 2026
- Fix
Select: the label and badge row in the trigger now applies to every Select, not only disabled ones
v1.0.0, Nov 11, 2026
- Fix
Library review, List: each family's select-all checkbox is always shown
v1.0.0, Nov 11, 2026