EDS 0.x
Components

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

PropTypeDefaultDescription
itemsRecord<string, ReactNode> or { value, label, … }[]Option registry; required when virtualized
placeholderstring"Select…"Trigger placeholder (search placeholder when searchable)
aria-labelstringAccessible name on the trigger
aria-labelledbystringID of an external label element
validation"none" | "error""none"Invalid (negative) border
fluidbooleantrueStretch trigger to container width
multiplebooleanfalseMulti-select with checkbox options
wrapSelectedbooleanfalseWrap multi chips onto new lines
virtualizedbooleanfalseCombobox under the hood + windowed list
searchablebooleantrue when virtualizedFilter input in the trigger
estimateSizenumber32Virtual row height (px); only when virtualized
overscannumber8Virtual list buffer rows; only when virtualized
emptyTextReactNode"No items found."Message when filter matches nothing (virtualized)
allowClearbooleanfalseClear control when a value is selected
value / defaultValuestring | string[] | nullControlled / uncontrolled value
onValueChange(value) => voidSelection change
onItemHighlighted(value, details) => voidVirtualized keyboard/pointer highlight
namestringForm field name
inputRefRef<HTMLInputElement>Hidden native input
disabled / readOnly / requiredbooleanfalseInteraction 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:

FieldTypeDescription
valuestringStable option id (array form)
labelReactNodePrimary row text
descriptionReactNodeSecondary line below the label
disabledbooleanNon-interactive row
iconStartReactNodeLeading icon slot
iconEndReactNodeTrailing icon slot

Record form: { [value: string]: ReactNode } — keys become value, values become label.

Accessibility

WCAG success criteria

CriterionLevelHow Select helps
2.1.1 KeyboardAFull open, navigate, select, clear, and dismiss without a pointer.
2.1.2 No Keyboard TrapAEscape closes the popup and restores focus; Tab moves out of the control.
2.4.3 Focus OrderATrigger → popup options → chip remove buttons follow a logical DOM / roving focus order.
2.4.7 Focus VisibleAA:focus-visible ring on the trigger (and focusable chip/clear controls).
4.1.2 Name, Role, ValueANamed trigger, listbox/options roles, selected/disabled/checked states via Base UI.
1.3.1 Info and RelationshipsALabel association via aria-labelledby; option descriptions render as secondary text in the row.
1.4.3 Contrast (Minimum)AALabel, placeholder, and list text use EDS text tokens on default backgrounds.
1.4.11 Non-text ContrastAATrigger border, focus ring, and error border meet contrast against adjacent colors.
3.2.1 On FocusAFocusing the trigger does not auto-submit or navigate; it only prepares for open.
3.2.2 On InputATyping in searchable mode filters options only — it does not change context (e.g. route change) without an explicit selection.
3.3.1 Error IdentificationAvalidation="error" marks the field visually; consumers must expose programmatic error text.
3.3.2 Labels or InstructionsARequire 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.

KeyWhenBehavior
⇥ Tab / + ⇥ TabClosedMove focus to / from the trigger (or combobox input).
↵ Enter / SpaceClosed, enabledOpen the listbox.
↵ Enter / SpaceOpenSelect the highlighted option (single) or toggle it (multiple).
/ ClosedOpen the list and highlight the first / last enabled option.
/ OpenMove highlight to the next / previous enabled option; disabled rows are skipped.
Home / EndOpenJump highlight to first / last enabled option.
⎋ EscOpenClose the popup; focus returns to the trigger.
Type charactersClosed, not searchableBase UI typeahead — jump to the next option whose label starts with the typed string.
Type charactersOpen + searchable / virtualizedFilter the list via the combobox input; arrow keys move within filtered results.
↵ Enter / SpaceChip remove button focusedRemove that chip from a multiple selection without opening the menu.
↵ Enter / SpaceClear 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.

PartSemantics
TriggerFocusable 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 listListbox containing option rows. Selected state and multi checkbox indicators are exposed by Base UI.
Disabled optionsdisabled: true on an item — skipped by keyboard and non-interactive for assistive tech.
Decorative iconsChevron, checkmarks, and option icons use aria-hidden="true".
Multiple + chipsSelected values render as chips; each remove button has aria-label="Remove {label}".
ClearallowClear renders a Clear control with aria-label="Clear".
Validationvalidation="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 inputname + inputRef expose a native input for forms and programmatic focus.
VirtualizedFull 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-visible on .eds-select__trigger uses 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 statevalidation="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 under dir="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 via aria-describedby on a wrapping field group.
  • Use disabled for non-interactive fields; use readOnly when the value should be visible but not editable.
  • Mark purely decorative option icons with aria-hidden in custom label nodes if you embed SVGs.
  • For virtualized, keep the full items array available — filtering and keyboard scroll depend on it.
  • Test multi-select chip remove and allowClear with 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:

  1. Keyboard only — Tab to the trigger, Enter to open, ArrowDown through three options, Enter to select, Escape to confirm close. Repeat with multiple and remove one chip via its button.
  2. 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.
  3. Screen reader — VoiceOver (Mac) or NVDA (Windows): confirm the trigger announces its name and role; open the list and hear option count / selection change.
  4. Error — Submit an empty required field: negative border and error text both visible; reader hears the error when focus moves to the field.
  5. Zoom & contrast — Browser zoom 200%; confirm focus ring and error border remain visible. Toggle light/dark theme on the portaled popup.
  6. RTL — Set dir="rtl" on a parent; confirm chevron, text alignment, and chip order follow reading direction.

On this page