Overview
<Select /> is the canonical form-control combobox — the intersection of <Input />'s
shell and <Menu />'s listbox. A single-select dropdown with the W3C Listbox + Combobox ARIA
pattern, type-ahead, keyboard navigation, native form integration via a hidden <input>, and a
trigger that matches Input / Textarea heights per size so they line up on the same form row.
Overview — country selector with realistic options
When to use
- A short list of mutually-exclusive options bound to a form field: country, plan, language.
- A category picker that needs grouping / labels / separators between sections.
- Anywhere a native
<select>would work but the design wants custom items (icons, two-line rows, themed chrome).
When NOT to use
- Multi-select with chips — use
<MultiSelect />when it ships. - Typeable filter / autocomplete — use
<Combobox />when it ships. - A free-form text input with suggestions —
<Combobox />. - A non-form chooser (single-pick preference inside a Menu) — use
<Menu.RadioGroup>instead. - The list is just actions, not values — use
<Menu />.
Anatomy
<Select>
<Select.Trigger />
<Select.Content>
<Select.Group>
<Select.Label> section header </Select.Label>
<Select.Item value="…"> option </Select.Item>
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.Item value="…"> option </Select.Item>
</Select.Group>
</Select.Content>
</Select><Select>
<Select.Trigger />
<Select.Content>
<Select.Group>
<Select.Label> section header </Select.Label>
<Select.Item value="…"> option </Select.Item>
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.Item value="…"> option </Select.Item>
</Select.Group>
</Select.Content>
</Select>Select— state owner + context provider. Ownsvalue,open, the item registry, the persistent value→label cache (so the trigger renders the right label even when the listbox is closed), and the lifecycle hooks (useEscapeStack,useOutsideClick). Renders a hidden<input type="hidden" name value>whennameis set so the field participates in<form>submission.Select.Trigger—role="combobox"+aria-haspopup="listbox". Renders the selected label (or placeholder) plus a chevron that rotates 180° when open. Reads visual axes from the root via context — no axis props of its own.Select.Content— portal-rendered, positioned, animated listbox. Defaults tomatchTriggerWidth={true}(Floating UI'ssizemiddleware syncs width to the trigger). Carries its own visual axis (solid/outline/soft) independent of the trigger's form-control variant.Select.Item—role="option"witharia-selected. Fires selection for both keyboard Enter/Space and mouse click. Renders a<Check>indicator on the logical end when selected.Select.ItemIndicator— standalone indicator slot for custom check marks; the default<Select.Item>renders its own Check, so most consumers won't reach for this directly.Select.Group—role="group"wrapper. Pair with<Select.Label>for a section header.Select.Label— non-interactive section heading.role="presentation".Select.Separator—role="separator"horizontal rule.
Variants
Trigger variants (form-control vocabulary)
The Trigger reuses Input / Textarea's 4 × 7 matrix verbatim.
| Variant | Chrome |
|---|---|
outline | 1px border + paper background. Default. |
solid | bg-subtle resting; pops to paper on focus. |
ghost | Borderless at rest; gains border + tint on hover/focus. |
underline | Bottom rule only; minimal chrome. |
Content variants (overlay vocabulary)
Same 3 as Popover / Menu, defaulting to solid. Independent from the Trigger's form-control
variant — mixing an underline trigger with a solid listbox is supported and reads naturally.
Sizes
Trigger heights match Input / Textarea per size — visual alignment on the same form row is a hard requirement.
| Size | Trigger height | Item padding | Font |
|---|---|---|---|
sm | h-8 | px-2 py-1 | text-xs |
md | h-10 | px-2 py-1.5 | text-sm |
lg | h-12 | px-3 py-2 | text-base |
Form integration
Pass name and the root renders <input type="hidden" name={name} value={value}> so the value
posts with the surrounding form. required triggers native form validation when no value is set
(combined with :invalid styling on the trigger via the aria-invalid bridge).
<form>
<Select name="country" required placeholder="Pick a country">
<Select.Trigger />
<Select.Content>
<Select.Item value="fr">France</Select.Item>
</Select.Content>
</Select>
<button type="submit">Subscribe</button>
</form><form>
<Select name="country" required placeholder="Pick a country">
<Select.Trigger />
<Select.Content>
<Select.Item value="fr">France</Select.Item>
</Select.Content>
</Select>
<button type="submit">Subscribe</button>
</form>Behavior
| Prop | Default | Effect |
|---|---|---|
closeOnEscape | true | Esc closes the listbox. |
closeOnOutsideClick | true | Pointer-down outside trigger + content closes the listbox. |
matchTriggerWidth | true | (on <Select.Content>) Listbox stretches to trigger width. |
loop | true | Arrow-key wrap at top/bottom. |
typeAhead | true | Type a prefix to jump to the matching item. |
placeholder | — | Shown in the trigger when no value is selected. |
Accessibility
ARIA Listbox + Combobox pattern (W3C APG):
- Trigger —
role="combobox"+aria-haspopup="listbox"+aria-expanded+aria-controls(only when open).role="combobox"is a nameFromAuthor role: passaria-label,aria-labelledby, or rely on the auto-fallback toplaceholderfor the accessible name. A surrounding<label htmlFor={triggerId}>works the same way it would for<input>. - Content —
role="listbox"+aria-labelledbythe trigger +aria-activedescendantset to the currently-highlighted item id while open (highlight, don't focus — same discipline as Menu). - Items —
role="option"+aria-selected+aria-disabledfor disabled items +data-highlighted="true"for the keyboard-highlighted row. useFormFieldA11ywiresid/aria-invalid/aria-required/aria-describedbyfrom the root onto the trigger — same hook Input / Textarea use.
Keyboard:
| Key | Action |
|---|---|
| Space / Enter | Toggle the listbox open / closed |
| ArrowDown / ArrowUp | Open + seed highlight to the selected (or first / last enabled) item |
| ArrowDown / ArrowUp (open) | Cycle highlight; wraps with loop=true |
| Home / End | First / last enabled item |
| Enter / Space (open) | Select highlighted item, close listbox |
| Esc | Close without selecting |
| Tab / Shift+Tab | Close + move focus to the next form field |
| Printable character | Type-ahead — prefix match within ~500 ms |
| Same letter twice | Cycle through items starting with that letter |
Focus management:
- Focus moves into Content on open (Content carries
tabIndex={-1}). - Focus returns to the trigger on close.
- The highlighted item auto-scrolls into view via
scrollIntoView({ block: 'nearest' })so keyboard nav past the visible window stays smooth.
axe-core: zero violations across every trigger variant and sampled colors, plus disabled / invalid / grouped states.
Examples
Basic usage — pick from a short list
WithGroups
WithIcons
Variants
Sizes
Colors
Disabled
Invalid
Required
DisabledItems
LongList
MatchTriggerWidth
PortalContainer
InsideField
Controlled
Theming
defineTheme({
components: {
Select: {
defaultProps: { /* root behavior props — loop / typeAhead / closeOnEscape */ },
styleOverrides: {
trigger: 'shadow-sm',
triggerChevron: '',
content: 'shadow-xl',
item: 'rounded-md',
itemIndicator: '',
label: '',
group: '',
separator: 'bg-border-strong',
},
},
},
});defineTheme({
components: {
Select: {
defaultProps: { /* root behavior props — loop / typeAhead / closeOnEscape */ },
styleOverrides: {
trigger: 'shadow-sm',
triggerChevron: '',
content: 'shadow-xl',
item: 'rounded-md',
itemIndicator: '',
label: '',
group: '',
separator: 'bg-border-strong',
},
},
},
});Per-instance overrides via <Select.Trigger className sx style /> and <Select.Content className sx style /> merge on top of the theme overrides, which merge on top of the recipe — same
precedence chain everywhere else in the DS.