Select
Form-associated searchable select with single/multi-select, chips, and full keyboard navigation. A Lit Web Component migrated from the Angular esa-select.
Preview
<esa-select label="Fruit" placeholder="Choose a fruit" help-text="Type to filter"></esa-select> Sizes
<esa-select label="Extra small" size="xs"></esa-select>
<esa-select label="Small" size="sm"></esa-select>
<esa-select label="Medium" size="md"></esa-select>
<esa-select label="Large" size="lg"></esa-select> States
Multi-select renders chips with chip-mode. Error, required, disabled, and read-only states are all driven by attributes.
<esa-select label="Toppings" multiple chip-mode placeholder="Add toppings"></esa-select>
<esa-select label="Required" required></esa-select>
<esa-select label="Error" error-text="Please make a selection"></esa-select>
<esa-select label="Disabled" disabled></esa-select> API
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | '' | Field label above the control. |
options | { label, value, disabled? }[] | [] | Selectable options. Set as a property (not an attribute). |
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Control size (reflected attribute). |
placeholder | string | 'Select...' | Placeholder shown when nothing is selected. |
help-text | string | '' | Helper text below the control. |
error-text | string | '' | Error message; sets the error state when present. |
required | boolean | false | Marks the field required (renders a * after the label). |
disabled | boolean | false | Disables interaction (reflected attribute). |
multiple | boolean | false | Allow multiple selections. |
searchable | boolean | true | Type-to-filter the option list. When false the input is read-only. |
chip-mode | boolean | false | In multi-select, render selected values as removable chips. |
value | string | string[] | — | Property accessor. Single = string; multiple = string array. |
Events
| Event | Type | Default | Description |
|---|---|---|---|
change | CustomEvent<{ value: string | string[] }> | — | Fired on selection change. Composed and bubbles. Form value is the string (single) or comma-joined values (multiple). |
Accessibility
- Input has
role="combobox"witharia-expanded,aria-haspopup="listbox", andaria-autocomplete="list". - Dropdown is
role="listbox"; options arerole="option"witharia-selected/aria-disabled. - Keyboard: ArrowUp/Down navigate, Enter selects, Escape/Tab close. Outside-click closes.
- Chip remove buttons carry an
aria-label("Remove {label}"). - Form-associated via
ElementInternals— value submits with the enclosing form.
Theming surface
Every public token this component reads, extracted from its source at build time.
The Resolves to column walks each token's real lineage —
component → semantic → primitive → raw value — so you
can see exactly where re-pointing it lands. A spoke re-skins by overriding these in
its theme-<slug>.css; component internals are never edited. No
hook for what you need? File it with /request-lego.
| Token | Tier | Resolves to |
|---|---|---|
--focus-ring-color | component | #65ba74 via --color-border-focus → --color-grass-8 |
--focus-ring-width | component | 2px |
--form-bg | component | #fcfcfc via --color-surface → --color-gray-1 |
--form-bg-disabled | component | #f0f0f0 via --color-disabled-bg → --color-gray-3 |
--form-border-color | component | #cecece via --color-border → --color-gray-7 |
--form-border-color-error | component | #e5484d via --color-danger → --color-red-9 |
--form-border-color-focus | component | #65ba74 via --color-border-focus → --color-grass-8 |
--form-border-width | component | 1px |
--form-error-color | component | #ce2c31 via --color-danger-strong → --color-red-11 |
--form-font-size-lg | component | clamp(0.875rem, 0.77rem + 0.52vw, 1.125rem) via --type-size-300 |
--form-font-size-md | component | clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --type-size-200 |
--form-font-size-sm | component | clamp(0.625rem, 0.56rem + 0.32vw, 0.75rem) via --type-size-100 |
--form-font-size-xs | component | clamp(0.5rem, 0.44rem + 0.3vw, 0.625rem) via --type-size-050 |
--form-height-lg | component | 48px |
--form-height-md | component | 40px |
--form-height-sm | component | 32px |
--form-height-xs | component | 28px |
--form-help-color | component | #838383 via --color-text-muted → --color-gray-10 |
--form-label-color | component | #646464 via --color-text-secondary → --color-gray-11 |
--form-label-font-size | component | var(--_field-font-size) |
--form-label-font-weight | component | 500 via --font-weight-medium |
--form-padding-x-lg | component | 1rem via --spacing-400 |
--form-padding-x-md | component | 0.75rem via --spacing-300 |
--form-padding-x-sm | component | 0.625rem via --spacing-250 |
--form-padding-x-xs | component | 0.5rem via --spacing-200 |
--form-padding-y-lg | component | 0.75rem via --spacing-300 |
--form-padding-y-md | component | 0.5rem via --spacing-200 |
--form-padding-y-sm | component | 0.375rem via --spacing-150 |
--form-padding-y-xs | component | 0.25rem via --spacing-100 |
--form-placeholder-color | component | #838383 via --color-text-muted → --color-gray-10 |
--form-radius-lg | component | 0.5rem via --radius-200 |
--form-radius-md | component | 0.5rem via --radius-200 |
--form-radius-sm | component | 0.25rem via --radius-100 |
--form-radius-xs | component | 0.25rem via --radius-100 |
--form-text-color | component | #202020 via --color-text-primary → --color-gray-12 |
--icon-size-medium | component | 20px |
--color-active-overlay | semantic | rgba(0, 88, 98, 0.08) |
--color-danger-border | semantic | #fdbdbe via --color-red-6 |
--color-danger-strong | semantic | #ce2c31 via --color-red-11 |
--color-disabled-text | semantic | #8d8d8d via --color-gray-9 |
--color-hover-overlay-strong | semantic | rgba(0, 0, 0, 0.05) |
--color-primary-strong | semantic | #2a7e3b via --color-grass-11 |
--color-surface | semantic | #fcfcfc via --color-gray-1 |
--color-surface-sunken | semantic | #f0f0f0 via --color-gray-3 |
--color-text-muted | semantic | #838383 via --color-gray-10 |
--color-text-primary | semantic | #202020 via --color-gray-12 |
--font-sans | semantic | 'DM Sans', sans-serif |
--transition-fast | semantic | 150ms ease |
--type-size-150 | semantic | clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) |
--radius-full | primitive | 9999px |
--shadow-200 | primitive | 0 4px 20px -4px rgba(0, 0, 0, 0.06) |
--spacing-050 | primitive | 0.125rem |
--spacing-100 | primitive | 0.25rem |
--spacing-200 | primitive | 0.5rem |
--spacing-300 | primitive | 0.75rem |
--z-dropdown | primitive | 50 |