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.

Type to narrow a longer list.

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-expanded and aria-controls; its popup contains a role="listbox" with role="option" items.
  • When open, focus moves to the search input (role="combobox"). It uses aria-activedescendant for 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 its aria-label. Hint or error is described to both controls.

API and code

NameTypeRequiredDescription
labelstring—Label rendered above the control.
hintstring—Helper text rendered below the control.
errorstring—Error message that replaces `hint` and enables error styling.
optionsComboboxOption[]—Available options.
valuestring—Selected value in controlled mode.
defaultValuestring—Initial selected value in uncontrolled mode.
onChange(value: string, option: ComboboxOption) => void—Called with the selected value and option.
placeholderstring—Trigger text when no option is selected. Default: `"Select…"`.
searchPlaceholderstring—Search input placeholder. Default: `"Search…"`.
emptyMessagestring—Text shown when filtering finds no options. Default: `"No results."`.
disabledboolean—Disables the trigger.
defaultOpenboolean—Whether the popup starts open. Useful for demos.
idstring—Id for the trigger. A stable id is generated when omitted.
classNamestring—Additional class name appended to `.lyra-combobox`.

x-data="lyraCombobox({ … })"

OptionTypeRequiredDescription
optionsLyraComboboxOption[]—Available data options. Default: `[]`.
valuestring—Initially selected option value; also the modelable selected value.
openboolean—Whether the popup initially starts open; also modelable. Default: `false`.
placeholderstring—Trigger text with no selected option. Default: `"Select…"`.
searchPlaceholderstring—Search input placeholder. Default: `"Search…"`.
emptyMessagestring—Text shown when filtering finds no options. Default: `"No results."`.
disabledboolean—Disables the trigger. Default: `false`.
idstring—Stable id base; one is generated from the root when omitted.
errorboolean—Applies the React field error modifier to the trigger. Default: `false`.
describedBystring—Consumer-owned field-message id used for `aria-describedby`.

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:

html
<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>

<lyra:combobox> Generated from lyra-ds/blade v0.10.0.

The behavior comes from lyraCombobox() — install @lyra-ds/alpine and see the HTML + Alpine tab.

PropDefaultRequiredExample values
options[]——
valuenull——
defaultOpenfalse——
labelnull—Country
hintnull—Choose one
errornull—Required
placeholdernull——
searchPlaceholdernull——
emptyMessagenull——
disabledfalse——
factory'lyraCombobox'——
extraOptions[]——
blade
<lyra:combobox
    label="Assignee"
    placeholder="Select a teammate"
    search-placeholder="Search teammates"
    empty-message="No teammate found."
    :options="[
        ['value' => 'ana', 'label' => 'Ana Ribeiro'],
        ['value' => 'joao', 'label' => 'João Martins'],
        ['value' => 'mei', 'label' => 'Mei Tanaka'],
    ]"
/>