Select
ImplementedNative select with visible label, helper text, and error state. Focus indication painted by the wrapper (focus-within ring); custom chevron. aria-describedby wired to helper/error. WCAG 2.5.5 tap-min enforced by wrapper height.
components/atoms/Select/Select.tsxWAI-ARIA listbox combobox with single/multi-select, type-ahead, optional search, async loading, and full keyboard navigation.
Select
Form input atom — Family 2 (Tier 1)
A controlled listbox (role="combobox") that supports single and multi-select, optional local search, debounced async loading, type-ahead, and a chip-rendered multi-value display. The trigger is a button; the listbox is a portal-free absolutely-positioned `<ul>` with active-descendant navigation.
Used for tax calculation.
Country is required.
This component supports light and dark mode.
Usage
Select is a controlled listbox. Pass options with stable valuekeys and always wire onChange. The component owns focus management, aria-activedescendant, type-ahead, Escape-to-close, and outside-click dismissal — never reimplement those on top of a native <select>.
import { useState } from 'react'
import { Select } from '@epasslive/design-system'
const COUNTRIES = [
{ value: 'ca', label: 'Canada' },
{ value: 'us', label: 'United States' },
{ value: 'fr', label: 'France' },
]
const [country, setCountry] = useState<string>('')
<Select
label="Country"
value={country}
onChange={setCountry}
options={COUNTRIES}
placeholder="Pick a country"
required
hint="Used for tax calculation."
/>Multi-select
Set multiple and pass an array as value. The component renders chips with individual remove buttons plus a "Clear all" affordance. The listbox receives aria-multiselectable="true" and each option renders a checkbox glyph.
const [tags, setTags] = useState<string[]>([])
<Select
label="Audience"
multiple
value={tags}
onChange={setTags}
options={TAG_OPTIONS}
placeholder="Add tags"
/>Async loading
Provide an asyncLoad(query) function. The component debounces 300 ms and flips to a spinner state while in-flight. Stale responses are dropped (a cancelled-flag in the effect closure) so a slow earlier query never overwrites a newer one.
<Select
label="Venue"
value={venueId}
onChange={setVenueId}
options={[]}
asyncLoad={async (q) => fetchVenues(q)}
searchable
/>Dark mode
The trigger swaps to ep-surface-card and the listbox to ep-surface-elevated with dark slate borders. The active-option highlight uses the teal token at 10% opacity so it stays readable on both themes. The chevron rotates 180° on open in both palettes.
Props
idstringField id; auto-generated with React.useId() when omitted.
label*stringVisible label above the trigger.
value*T | T[]Controlled value. Pass a single value for single mode, an array for multi mode.
onChange*(value: T | T[]) => voidChange handler. Receives the new value (or array of values in multi mode).
options*SelectOption<T>[]Available options. Each has value, label, and optional disabled flag.
placeholderstringPlaceholder text shown when no value is selected.
multiplebooleanfalseEnables multi-select (chip rendering, clear-all, checkbox glyphs).
searchablebooleanfalseEmbeds a search input at the top of the listbox that filters options locally.
asyncLoad(query: string) => Promise<SelectOption<T>[]>Optional async loader; debounced 300 ms. Replaces local options when set.
errorstringWhen set, swaps the trigger border to error, wires aria-invalid, and shows the message under the trigger.
hintstringHelper text rendered below the trigger when there is no error.
disabledbooleanfalseDisables the trigger and chips.
requiredbooleanfalseMarks the field required (asterisk + native aria-required).
size'sm' | 'md'mdTrigger height; sm = h-8, md = h-10.
ariaLabelstringOverride for the trigger aria-label when label is not visible.
Props
Select
labelstring—Visible field label.
valuestring—Controlled value.
onChange(value: string) => void—Change handler.
optionsSelectOption[]—Option list ({label, value, disabled?}).
placeholderstring—Hidden disabled first option.
helperTextstring—Helper wired via aria-describedby.
errorstring—Error text; shows error border + role=alert.
requiredbooleanFalseRequired marker + aria-required.
<Select />Accessibility
ARIA
1 ruleType
1 ruleUsage
Import
import { Select } from "@epasslive/design-system"Example 1
<Select label={t('venue')} value={v} onChange={setV} options={opts} />Anti-patterns
— Do not use for search-as-you-type — SearchSelect is the searchable variant