Combobox
Combobox is the searchable choice control for lists that are too long to scan at once. It keeps DOM
focus in its search input while aria-activedescendant identifies the active option, so filtering and
keyboard movement do not move focus through the list.
Examples
Searchable options
Use short labels people can predict while typing. Optional option hint adds enough context to
tell similar choices apart without turning the result into a second paragraph.
Controlled selection
Pass value and onChange when another part of the view owns the choice. Without value,
defaultValue seeds the component's own selected value.
When to use
Use a Combobox for one choice from a known list when filtering makes finding that choice materially faster.
Reach for something else when:
- The list is short and does not need filtering — use Select and retain the browser's native picker.
- A few mutually exclusive options should be compared at a glance — use Radio rather than hiding them.
- People are entering a value that is not limited to options — use Input or Textarea, depending on length.
Accessibility
- The trigger exposes
aria-haspopup="listbox",aria-expandedandaria-controls; its popup contains arole="listbox"withrole="option"items. - When open, focus moves to the search input (
role="combobox"). It usesaria-activedescendantfor the active option and resets that active option after filtering. - Arrow Up/Down changes the active option; Enter selects it; Escape closes the popup and restores focus to the trigger.
- A label names the search input through
aria-labelledby; without one, the search placeholder is used as itsaria-label. Hint or error is described to both controls.
Props
| Name | Type | Required | Description |
|---|---|---|---|
label | string | — | Label rendered above the control. |
hint | string | — | Helper text rendered below the control. |
error | string | — | Error message that replaces `hint` and enables error styling. |
options | ComboboxOption[] | — | Available options. |
value | string | — | Selected value in controlled mode. |
defaultValue | string | — | Initial selected value in uncontrolled mode. |
onChange | (value: string, option: ComboboxOption) => void | — | Called with the selected value and option. |
placeholder | string | — | Trigger text when no option is selected. Default: `"Select…"`. |
searchPlaceholder | string | — | Search input placeholder. Default: `"Search…"`. |
emptyMessage | string | — | Text shown when filtering finds no options. Default: `"No results."`. |
disabled | boolean | — | Disables the trigger. |
defaultOpen | boolean | — | Whether the popup starts open. Useful for demos. |
id | string | — | Id for the trigger. A stable id is generated when omitted. |
className | string | — | Additional class name appended to `.lyra-combobox`. |
Plain HTML
Keep the trigger, search input and listbox ids aligned. DOM focus remains on the search input;
aria-activedescendant identifies the active option without moving focus into the list:
<div class="lyra-field">
<label class="lyra-label" id="country-label" for="country">Country</label>
<span class="lyra-combobox">
<button
class="lyra-input lyra-combobox__trigger"
id="country"
type="button"
aria-haspopup="listbox"
aria-expanded="true"
aria-controls="country-listbox"
>
<span class="lyra-combobox__placeholder">Choose a country</span>
</button>
<div class="lyra-combobox__pop">
<div class="lyra-combobox__search">
<input
role="combobox"
aria-expanded="true"
aria-controls="country-listbox"
aria-autocomplete="list"
aria-activedescendant="country-option-0"
aria-labelledby="country-label"
/>
</div>
<div class="lyra-combobox__list" id="country-listbox" role="listbox">
<button
class="lyra-combobox__option lyra-combobox__option--active"
id="country-option-0"
role="option"
tabindex="-1"
aria-selected="false"
>
Brazil
</button>
</div>
</div>
</span>
</div>