Skip to content

Select

A labelled choice from a list, built on a native <select>.

import { Select } from '@zitdevs/ui';

A plain select is the native control, styled. It needs no script at all.

A range is enough.

Choose a budget range.

For a long list, searchable adds the design’s filterable list. These demos are server HTML; the page runs enhanceSelects(document) from @zitdevs/ui/enhance, as the Astro website does. Type to filter — accents and case do not matter — move with the arrow keys, choose with Enter or a click, close with Escape.

<Select
id="country"
label="Country"
options={countries}
placeholder="Choose a country"
autoComplete="country-name"
searchable
/>

A caller that holds the choice passes value and onValueChange. Searchable, a choice only asks: the trigger changes when value does, and the native select fires change for the form. Plain, it is a controlled native select.

'use client';
const [country, setCountry] = useState('Peru');
<Select
id="country"
label="Country"
options={countries}
searchable
value={country}
onValueChange={setCountry}
/>;
Prop Type Default Notes
id string required The native select’s id.
name string id Submitted under it.
label string required
options readonly SelectOption[] required A string, or { value, label?, disabled?, lang? }.
autoComplete string — What the answer is, such as country-name, whenever it asks about the reader.
defaultValue string — Matched against values, not labels. A new one starts the select over.
value string — The chosen value, for a caller that holds it.
onValueChange (value: string) => void — The value the reader chooses. Controlled, it asks; uncontrolled, it reports.
placeholder string — Shown while nothing is chosen; never submitted.
requirement 'none' | 'required' | 'optional' 'none' As on Field.
hint, error string — The error replaces the hint and marks the control invalid.
messageId string `${id}-message` For a form whose ids are fixed.
disabled boolean false
searchable boolean false Adds the filterable list. Needs the enhancer.
searchPlaceholder string labels.search Names the filter field.
labels Partial<SelectLabels> English { optional, search, noResults }. es.select.
ref Ref<HTMLSelectElement> — The native select.
className string — Placement, on the wrapper.
anything else <select> attributes — On the native select: form, onChange (it fires for the list’s choices too).

Exported types: SelectProps, SelectOption, SelectLabels, and the label sets selectLabels. From @zitdevs/ui/enhance: enhanceSelects, SelectOptions, SelectEnhancement.

  • Plain, it is the native select: every platform’s own picker, keyboard and screen reader support.
  • Searchable, it follows the ARIA combobox pattern: a button that opens a listbox, with a filter field that points at the active option through aria-activedescendant. Arrow keys, Home and End move; Enter chooses; Escape closes and returns focus; Tab and a click elsewhere close.
  • The native select stays in the form, named and enabled, so it validates and submits; it is taken off screen and out of the accessibility tree while the list stands in for it. A click on the label or a failed check sends focus to the control the reader can see, and a failed check marks it invalid until a valid choice.
  • An answer in another language carries its lang (WCAG 3.1.2), in the native and the listed option.
  • A placeholder is a disabled first option: shown, never submitted.

A plain select needs none. A searchable one is the plain native select until the enhancer runs, which is a complete control on its own:

  • React: nothing to do. The select’s root is a small client part that runs the enhancer once it has hydrated. A plain Select ships no JavaScript.
  • No React: load @zitdevs/ui/enhance and call enhanceSelects(document) once. A page that also hydrates a Select island scopes the call to its static part.

fieldControl’s roles, muted for the prompt, ink and border-strong for the markers; surface-raised and border-default for the list, surface for the filter until it takes focus, primary-soft for the active option and primary for the check.