COMBOBOX: Filter-as-you-type single-select (ARIA 1.2)
COMPOSES: .form-control | .dropdown-menu | .dropdown-item

Typing filters the list; arrows move the active option; Enter selects; Escape restores the committed value. Free text that matches no option rolls back on close — the field can only ever hold a real option (error prevention, C05).

Basic

Basic: label + visible input + hidden value field. The hidden input (data-combobox-target="value") is the form field; the visible input carries only the display text.

<div data-controller="combobox"><div class="combobox"><input class="form-control" role="combobox" …><ul class="dropdown-menu" role="listbox">…</ul></div></div>

Async (remote source)

Async mode: set data-combobox-url-value and the controller debounces input, then fetches {url}?q={query} and renders the JSON {value, text}[] response as options — same commit, keyboard, and rollback contract as the static list above. Below data-combobox-min-chars-value a .combobox-hint row prompts for more input; while a request is pending a .combobox-loading row (.spinner-border-sm) shows; a failed fetch reuses .combobox-empty with retry copy. A persistent visually-hidden data-combobox-target="status" span announces each transition ("Type at least N characters…", "Searching…", "N results available", "No results found", or the error copy) to screen readers — recommended over aria-live on the visually-toggled rows themselves, since a live-region node that gets hidden toggled on/off is an unreliable announcement trigger.

Preview mock: these examples have no real backend, so each carries an inline <script type="application/json" data-combobox-target="fixture"> sibling (a static {results, delay, error} fixture). When present, the controller reads it instead of calling fetch — same visible delay and superseded-request behavior, though the mechanics differ slightly (the fixture cancels its timer on abort rather than rejecting like a real aborted fetch). Production usage simply omits the fixture and points url at a real same-origin endpoint.

Try: 1 letter (hint) → 2+ letters of a school name (loading → results) → zzz (no results).

This example's fixture always simulates a failed request — type any letter to see the loading row, then the retry-copy error state.

<div data-controller="combobox" data-combobox-url-value="/api/schools/search" data-combobox-min-chars-value="2" data-combobox-debounce-value="250">…<span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span>…<li class="combobox-hint" hidden data-combobox-target="hint">…<li class="combobox-loading" hidden data-combobox-target="loading"><span class="spinner-border spinner-border-sm"></span>…</li></div>

Preselected value

Preselected: set the visible input's value, the hidden input's value, and .active on the matching option.