Select
Examples
Default
Fluid vs hug
fluid (default) stretches the trigger to the container. Set fluid={false} to hug content.
States
Validation
Figma Validation=Invalid (Negative) — use validation="error".
Multi-select
Multi options use a checkbox-style indicator. Selected values render as chips; set wrapSelected to wrap onto new lines.
Controlled
Allow clear
Hover the trigger to reveal the clear control (replaces the chevron).
TanStack Form
Submit without a selection to see validation. Choosing a plan and submitting again succeeds.
Virtualized (10,000 items)
Pass virtualized with a full items array. Only a window of options mounts in the DOM; keyboard highlight scrolls the window via Base UI’s onItemHighlighted. Requires peer @tanstack/react-virtual. Tune row height with estimateSize, buffer with overscan, and the empty-filter message with emptyText.
Selected: item-1 · 10,000 items
RTL
Wrap the control (or a parent) in dir="rtl" / lang="fa". Trigger text, chevron, and menu items follow reading direction. Portaled menus inherit dir so virtualized lists stay RTL too.
API Reference
Select
| Prop | Type | Default | Description |
|---|---|---|---|
items | Record<string, ReactNode> or { value, label, … }[] | — | Option registry; required when virtualized |
placeholder | string | "Select…" | Trigger placeholder (search placeholder when searchable) |
aria-label | string | — | Accessible name on the trigger |
aria-labelledby | string | — | ID of an external label element |
validation | "none" | "error" | "none" | Invalid (negative) border |
fluid | boolean | true | Stretch trigger to container width |
multiple | boolean | false | Multi-select with checkbox options |
wrapSelected | boolean | false | Wrap multi chips onto new lines |
virtualized | boolean | false | Combobox under the hood + windowed list |
searchable | boolean | true when virtualized | Filter input in the trigger |
estimateSize | number | 32 | Virtual row height (px); only when virtualized |
overscan | number | 8 | Virtual list buffer rows; only when virtualized |
emptyText | ReactNode | "No items found." | Message when filter matches nothing (virtualized) |
allowClear | boolean | false | Clear control when a value is selected |
value / defaultValue | string | string[] | null | — | Controlled / uncontrolled value |
onValueChange | (value) => void | — | Selection change |
onItemHighlighted | (value, details) => void | — | Virtualized keyboard/pointer highlight |
name | string | — | Form field name |
inputRef | Ref<HTMLInputElement> | — | Hidden native input |
disabled / readOnly / required | boolean | false | Interaction flags |
Requires peer @tanstack/react-virtual when using virtualized.
items option fields
Each entry in the items array (or key/value pair in a record) defines one option:
| Field | Type | Description |
|---|---|---|
value | string | Stable option id (array form) |
label | ReactNode | Primary row text |
description | ReactNode | Secondary line below the label |
disabled | boolean | Non-interactive row |
iconStart | ReactNode | Leading icon slot |
iconEnd | ReactNode | Trailing icon slot |
Record form: { [value: string]: ReactNode } — keys become value, values become label.
Accessibility
WCAG success criteria
| Criterion | Level | How Select helps | |
|---|---|---|---|
| 2.1.1 Keyboard | A | Full open, navigate, select, clear, and dismiss without a pointer. | |
| 2.1.2 No Keyboard Trap | A | Escape closes the popup and restores focus; Tab moves out of the control. | |
| 2.4.3 Focus Order | A | Trigger → popup options → chip remove buttons follow a logical DOM / roving focus order. | |
| 2.4.7 Focus Visible | AA | :focus-visible ring on the trigger (and focusable chip/clear controls). | |
| 4.1.2 Name, Role, Value | A | Named trigger, listbox/options roles, selected/disabled/checked states via Base UI. | |
| 1.3.1 Info and Relationships | A | Label association via aria-labelledby; option descriptions render as secondary text in the row. | |
| 1.4.3 Contrast (Minimum) | AA | Label, placeholder, and list text use EDS text tokens on default backgrounds. | |
| 1.4.11 Non-text Contrast | AA | Trigger border, focus ring, and error border meet contrast against adjacent colors. | |
| 3.2.1 On Focus | A | Focusing the trigger does not auto-submit or navigate; it only prepares for open. | |
| 3.2.2 On Input | A | Typing in searchable mode filters options only — it does not change context (e.g. route change) without an explicit selection. | |
| 3.3.1 Error Identification | A | validation="error" marks the field visually; consumers must expose programmatic error text. | |
| 3.3.2 Labels or Instructions | A | Require aria-label / aria-labelledby; use placeholder as hint only. |
Why it matters
Select is one of the most common form controls — and one of the easiest to ship without a usable name, keyboard path, or error association. Keyboard and switch users rely on predictable open/close, arrow navigation, and typeahead. Screen-reader users need the trigger named, the list exposed as options, and selection state announced. A missing label or orphaned error message blocks task completion and fails WCAG at scale.
Keyboard
Focus lands on the trigger (or the filter input when searchable / virtualized). The popup is portaled but stays in the tab order while open; ⎋ Esc always closes and returns focus to the trigger.
| Key | When | Behavior |
|---|---|---|
| ⇥ Tab / ⇧ + ⇥ Tab | Closed | Move focus to / from the trigger (or combobox input). |
| ↵ Enter / Space | Closed, enabled | Open the listbox. |
| ↵ Enter / Space | Open | Select the highlighted option (single) or toggle it (multiple). |
| ↓ / ↑ | Closed | Open the list and highlight the first / last enabled option. |
| ↓ / ↑ | Open | Move highlight to the next / previous enabled option; disabled rows are skipped. |
| Home / End | Open | Jump highlight to first / last enabled option. |
| ⎋ Esc | Open | Close the popup; focus returns to the trigger. |
| Type characters | Closed, not searchable | Base UI typeahead — jump to the next option whose label starts with the typed string. |
| Type characters | Open + searchable / virtualized | Filter the list via the combobox input; arrow keys move within filtered results. |
| ↵ Enter / Space | Chip remove button focused | Remove that chip from a multiple selection without opening the menu. |
| ↵ Enter / Space | Clear control (allowClear) | Clear the current value. |
Disabled — trigger is removed from the tab order; no open interaction. Read-only — focusable for context but value cannot change. Virtualized — arrow highlight fires onItemHighlighted; the virtual window scrolls so the highlighted row stays visible even when only a slice of DOM nodes exists.
Screen readers & semantics
EDS exposes a props-first API — consumers pass aria-label or aria-labelledby on <Select>; there is no public trigger primitive to wire separately.
| Part | Semantics |
|---|---|
| Trigger | Focusable control with an accessible name from aria-label / aria-labelledby. Base UI maps to combobox when searchable or virtualized, otherwise select / combobox per Base UI Select. |
| Popup list | Listbox containing option rows. Selected state and multi checkbox indicators are exposed by Base UI. |
| Disabled options | disabled: true on an item — skipped by keyboard and non-interactive for assistive tech. |
| Decorative icons | Chevron, checkmarks, and option icons use aria-hidden="true". |
| Multiple + chips | Selected values render as chips; each remove button has aria-label="Remove {label}". |
| Clear | allowClear renders a Clear control with aria-label="Clear". |
| Validation | validation="error" sets data-validation="error" for a negative border only — you must render visible error text and wire aria-describedby / aria-invalid at the form level (see TanStack Form example). |
| Hidden input | name + inputRef expose a native input for forms and programmatic focus. |
| Virtualized | Full items registry stays in memory; only a windowed subset mounts. Screen readers traverse the rendered options; filtering reduces the visible set without losing the registry. |
Always provide one naming strategy: visible <label htmlFor> paired with aria-labelledby, or aria-label when no visible label exists. Placeholder text is not a substitute for a label.
Focus & visuals
- Focus ring —
:focus-visibleon.eds-select__triggeruses a token-based box shadow (select.css) so keyboard focus is obvious without shifting layout. - Open state — trigger shows an open ring; popup is portaled with theme scope inherited from the Select root so light/dark tokens stay correct.
- Error state —
validation="error"applies a negative border token; pair with adjacent error copy for color-blind users. - Contrast — text, borders, and icons use
--eds-color-*semantic tokens designed for AA contrast on default surfaces. - RTL — logical layout (
inline-start/inline-end) mirrors trigger content, chevron, chips, and list alignment underdir="rtl". Portaled popups inherit direction. - Reduced motion — list open/close follows Base UI defaults; no gratuitous motion on selection.
Build with it
Do
- Give every Select a visible label or
aria-label/aria-labelledby. - Set
validation="error"when invalid and render error text linked viaaria-describedbyon a wrapping field group. - Use
disabledfor non-interactive fields; usereadOnlywhen the value should be visible but not editable. - Mark purely decorative option icons with
aria-hiddenin customlabelnodes if you embed SVGs. - For
virtualized, keep the fullitemsarray available — filtering and keyboard scroll depend on it. - Test multi-select chip remove and
allowClearwith a screen reader.
Don't
- Rely on placeholder alone — it disappears once a value is chosen and is not a accessible name.
- Hide the only copy of a field label inside the trigger without
aria-labelledby. - Use
validation="error"without visible, programmatically associated error messaging. - Disable options without indicating why in surrounding UI when the reason matters.
- Assume portaled popups break RTL — wrap the page or field in
dir="rtl"and verify the menu mirrors correctly.
Quick test
Five-minute pass you can run on any Select instance:
- Keyboard only — Tab to the trigger, Enter to open, ArrowDown through three options, Enter to select, Escape to confirm close. Repeat with
multipleand remove one chip via its button. - Typeahead — On a non-searchable Select, focus closed and type the first letter of an option; confirm highlight jumps. On
virtualized, type to filter and arrow through results. - Screen reader — VoiceOver (Mac) or NVDA (Windows): confirm the trigger announces its name and role; open the list and hear option count / selection change.
- Error — Submit an empty required field: negative border and error text both visible; reader hears the error when focus moves to the field.
- Zoom & contrast — Browser zoom 200%; confirm focus ring and error border remain visible. Toggle light/dark theme on the portaled popup.
- RTL — Set
dir="rtl"on a parent; confirm chevron, text alignment, and chip order follow reading direction.