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| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | Visible label. Always rendered. Accessibility: Associated with the trigger (label/for on web; accessibilityLabel on native). |
| name required | string | — | Field name for the Form. |
| options required | array ListboxItem[] (options and one level of groups, as Listbox) | — | The options, passed through to the Listbox. |
| value | string | string[] | — | Controlled value (array with `multiple`). |
| defaultValue | string | string[] | — | Initial value (array with `multiple`). |
| placeholder | string | — | Text shown in the trigger when nothing is selected. Defaults to `copy.placeholder`. Not a substitute for the label. |
| hideLabel | boolean | false | Visually hide the label (it remains the accessible name), for compact pickers such as DatePicker's month and year. |
| size | sm | md | md | sm for pickers inside toolbars and calendar headers. |
| open | boolean | — | Controlled popup state, for programmatic opening and for stories and tests (the Keyboard story renders it open). Omit for the trigger-driven default. |
| multiple | boolean | false | 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. |
| description | string | — | Helper text under the label. Accessibility: aria-describedby / accessibilityHint. |
| required | boolean | false | Must have a value to submit. Shown in the label, not only by color. |
| disabled | boolean | false | 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. |
| invalid | boolean | false | Marks the field invalid. Usually set by the Form. |
| error | string | — | Error message; implies invalid. Accessibility: role=alert region linked with aria-describedby. |
| native | auto | always | never | auto | 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. |
Events
2 rows| Event | Type | Description |
|---|---|---|
| onChange | onChange / change | Fired when the value changes (array with `multiple`). |
| onOpenChange | onOpenChange / open-change | Fired when the popup opens or closes. |
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
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.
This component does not animate, so it has no reduced-motion behaviour to describe.