Select

A form field that picks one option (or several) from a list opened on demand — a labelled trigger that shows the current value and opens a Listbox in a popup, or a native picker where that is what people expect.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Select } from '@design-schema/react';

Examples

import { Select, type ListboxOption } from '@design-schema/react';

const COUNTRIES: ListboxOption[] = [
  { value: 'ca', label: 'Canada' },
  { value: 'fr', label: 'France' },
  { value: 'de', label: 'Germany' },
  { value: 'jp', label: 'Japan' },
  { value: 'mx', label: 'Mexico' },
  { value: 'us', label: 'United States' },
];

<Select label="Country" name="country" options={COUNTRIES} />

Props

16 rows
PropTypeDefaultDescriptionPlatforms
label requiredstring—

Visible label. Always rendered.

Accessibility: Associated with the trigger (label/for on web; accessibilityLabel on native).

web, lit, rn, swiftui
name requiredstring—

Field name for the Form.

web, lit, rn, swiftui
options requiredarray ListboxItem[] (options and one level of groups, as Listbox)—

The options, passed through to the Listbox.

web, lit, rn, swiftui
valuestring | string[]—

Controlled value (array with `multiple`).

web, lit, rn, swiftui
defaultValuestring | string[]—

Initial value (array with `multiple`).

web, lit, rn, swiftui
placeholderstring—

Text shown in the trigger when nothing is selected. Defaults to `copy.placeholder`. Not a substitute for the label.

web, lit, rn, swiftui
hideLabelbooleanfalse

Visually hide the label (it remains the accessible name), for compact pickers such as DatePicker's month and year.

web, lit, rn, swiftui
sizesm | mdmd

sm for pickers inside toolbars and calendar headers.

web, lit, rn, swiftui
openboolean—

Controlled popup state, for programmatic opening and for stories and tests (the Keyboard story renders it open). Omit for the trigger-driven default.

web, lit, rn, swiftui
multiplebooleanfalse

Pick any number. The trigger shows `copy.selectedCount` (or the labels when two or fewer); the popup stays open while toggling and closes on Escape or outside click.

web, lit, rn, swiftui
descriptionstring—

Helper text under the label.

Accessibility: aria-describedby / accessibilityHint.

web, lit, rn, swiftui
requiredbooleanfalse

Must have a value to submit. Shown in the label, not only by color.

web, lit, rn, swiftui
disabledbooleanfalse

Not openable and not submitted. Stays visible and focusable. Wins over a controlled `open`: a disabled Select never shows its popup — it forces the popup closed locally, reports `aria-expanded="false"` so it never announces an expansion the user cannot see, and fires no `onOpenChange` to correct the caller's prop.

web, lit, rn, swiftui
invalidbooleanfalse

Marks the field invalid. Usually set by the Form.

web, lit, rn, swiftui
errorstring—

Error message; implies invalid.

Accessibility: role=alert region linked with aria-describedby.

web, lit, rn, swiftui
nativeauto | always | neverauto

Use the platform's own picker instead of the popup Listbox: `auto` means never on web (the styled popup) and always on native phones (the OS wheel/dialog is what users expect); `always` forces a native <select> on web too (forms that must work without JS); `never` forces the popup everywhere. On web and Lit `auto` and `never` therefore render identically and only `always` differs, so two of the three derived stories carry no web signal — that is expected, not a gap.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onChangeonChange / change

Fired when the value changes (array with `multiple`).

web, lit, rn, swiftui
onOpenChangeonOpenChange / open-change

Fired when the popup opens or closes.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply the label. It is associated with the control for you, but the words are yours — a placeholder is not a label.

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

Supply the error text when the value is invalid. The component links and announces it; it cannot write it.

Required props that carry accessibility meaning: label. Each is described in the props table above.

Implicit ARIA role: combobox.

Anatomy

labeldescriptiontriggervaluechevronpopuplistboxerrorMessage

What generation guarantees

Label association — The label is programmatically associated with the control, not merely placed beside it.

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Expanded state — Open and closed are exposed programmatically (`aria-expanded`), not only visually.

Selected state — Selection is exposed programmatically (`aria-selected` / `aria-checked`).

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Escape dismisses — Escape closes it from anywhere inside.

Focus restore — On close, focus returns to whatever opened it.

Error identification — An invalid value is identified in text and linked to the control — never signalled by colour alone.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Target size — Interactive targets are at least 44×44 CSS pixels — the touch-first size, not the minimum.

Select keyboard support

6 rows
KeyAction
Enter or Space or ArrowDown or ArrowUpOpens the popup with the selected (or first) option active. (focus on trigger)
EscapeCloses the popup without changing the value and returns focus to the trigger. (popup open)
EnterCommits the active option and closes (single); with `multiple`, toggles it and stays open. (popup open)
TabCommits the active option (single) and closes; focus moves on. (popup open)
SpaceCommits the active option and closes (single); with `multiple`, toggles it and stays open. Listbox's own model, which Select forwards rather than suppresses. (popup open)
ArrowDown or ArrowUp or Home or End or a-zAs Listbox. (popup open)

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.foregroundcolor.backgroundAA
color.foreground.mutedcolor.backgroundAA
color.foreground.dangercolor.backgroundAA
color.border.strongcolor.backgroundAA (non-text)

This component does not animate, so it has no reduced-motion behaviour to describe.