Combobox and autocomplete
Preview Available in release assets
Enhance a native text field with bounded ARIA listbox navigation while keeping typed values, submission, and server-provided results durable.
Combobox and autocomplete primary example
<form action="/locations" method="get">
<div class="thin-combobox" data-enhance="combobox">
<label for="location">Location</label>
<input class="thin-input" id="location" name="location" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-results" aria-describedby="location-status" />
<ul class="thin-combobox-listbox" id="location-results" role="listbox">
<li id="location-chi" role="option" data-value="Chicago, Illinois">Chicago, Illinois</li>
<li id="location-chihuahua" role="option" data-value="Chihuahua, Mexico">Chihuahua, Mexico — a deliberately long result label that wraps</li>
</ul>
<small class="thin-combobox-status" id="location-status">2 server-provided suggestions. Type any location or choose a suggestion.</small>
</div>
<button class="thin-button">Search</button>
</form>Show code
<form action="/locations" method="get">
<div class="thin-combobox" data-enhance="combobox">
<label for="location">Location</label>
<input class="thin-input" id="location" name="location" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-results" aria-describedby="location-status" />
<ul class="thin-combobox-listbox" id="location-results" role="listbox">
<li id="location-chi" role="option" data-value="Chicago, Illinois">Chicago, Illinois</li>
<li id="location-chihuahua" role="option" data-value="Chihuahua, Mexico">Chihuahua, Mexico — a deliberately long result label that wraps</li>
</ul>
<small class="thin-combobox-status" id="location-status">2 server-provided suggestions. Type any location or choose a suggestion.</small>
</div>
<button class="thin-button">Search</button>
</form>Variant gallery
Each authored example uses the production assets and includes copyable markup.
Free text with authored suggestions
The element coordinates the supplied list; it does not fetch or filter data. Focus opens the suggestions. Down selects an active option, Enter chooses it and Escape closes. Any typed location can be submitted normally.
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-free">Location</label>
<input class="thin-input" id="location-free" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-free-results" aria-describedby="location-free-status" value="">
<ul class="thin-combobox-listbox" id="location-free-results" role="listbox" aria-label="Suggested locations">
<li id="location-free-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-free-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-free-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-free-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-free">Location</label>
<input class="thin-input" id="location-free" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-free-results" aria-describedby="location-free-status" value="">
<ul class="thin-combobox-listbox" id="location-free-results" role="listbox" aria-label="Suggested locations">
<li id="location-free-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-free-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-free-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-free-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Server-filtered response snapshot
An example of HTML returned after an ordinary GET search for Chi. Your server supplies both the field value and suggestions. Replacing the list refreshes the observed host.
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-server">Location</label>
<input class="thin-input" id="location-server" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-server-results" aria-describedby="location-server-status" value="Chi">
<ul class="thin-combobox-listbox" id="location-server-results" role="listbox" aria-label="Suggested locations">
<li id="location-server-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-server-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-server-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-server-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-server">Location</label>
<input class="thin-input" id="location-server" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-server-results" aria-describedby="location-server-status" value="Chi">
<ul class="thin-combobox-listbox" id="location-server-results" role="listbox" aria-label="Suggested locations">
<li id="location-server-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-server-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-server-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-server-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Anatomy
- Explicit label
- Native named text input with role=combobox
- Server-authored listbox
- Options with durable values
- Status or error text
- Submit button
Variants and sizes
- Free text with suggestions — shown above
- Server-filtered autocomplete — shown above
States and difficult content
Rendered fixtures exercise authored content and constrained layouts with the same production assets.
Closed, open and active suggestion
Starts closed after enhancement connects. Focus the input to open it, use Down to activate a suggestion and Escape to close. DOM focus remains in the native input.
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-states">Location</label>
<input class="thin-input" id="location-states" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-states-results" aria-describedby="location-states-status" value="">
<ul class="thin-combobox-listbox" id="location-states-results" role="listbox" aria-label="Suggested locations">
<li id="location-states-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-states-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-states-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-states-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-states">Location</label>
<input class="thin-input" id="location-states" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-states-results" aria-describedby="location-states-status" value="">
<ul class="thin-combobox-listbox" id="location-states-results" role="listbox" aria-label="Suggested locations">
<li id="location-states-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-states-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-states-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-states-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>No matching suggestions
A server-authored empty-result response. The persistent status explains the result while ordinary form submission remains available.
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-empty">Location</label>
<input class="thin-input" id="location-empty" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-empty-results" aria-describedby="location-empty-status" value="No matching place">
<ul class="thin-combobox-listbox" id="location-empty-results" role="listbox" aria-label="Suggested locations">
</ul>
<p class="thin-combobox-status" id="location-empty-status">No suggestions match this query. Submit a different location to search again.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-empty">Location</label>
<input class="thin-input" id="location-empty" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-empty-results" aria-describedby="location-empty-status" value="No matching place">
<ul class="thin-combobox-listbox" id="location-empty-results" role="listbox" aria-label="Suggested locations">
</ul>
<p class="thin-combobox-status" id="location-empty-status">No suggestions match this query. Submit a different location to search again.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Server error with durable search
A server-rendered error is connected to the input. The application owns clearing aria-invalid and replacing the status on its next successful response.
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-error">Location</label>
<input class="thin-input" id="location-error" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-error-results" aria-describedby="location-error-status" value="Chicago" aria-invalid="true">
<ul class="thin-combobox-listbox" id="location-error-results" role="listbox" aria-label="Suggested locations">
</ul>
<p class="thin-combobox-status" id="location-error-status" role="alert">Suggestions could not be loaded. You can still submit the typed location.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<search aria-label="Location search" style="overflow-wrap: anywhere; ">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-error">Location</label>
<input class="thin-input" id="location-error" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-error-results" aria-describedby="location-error-status" value="Chicago" aria-invalid="true">
<ul class="thin-combobox-listbox" id="location-error-results" role="listbox" aria-label="Suggested locations">
</ul>
<p class="thin-combobox-status" id="location-error-status" role="alert">Suggestions could not be loaded. You can still submit the typed location.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Long, disabled and replaced suggestions
The popup wraps inside a narrow search. Disabled suggestions are skipped. When replacing server-authored result HTML, retain input and option IDs so the coordinator can preserve focus and the active suggestion. Browser checks exercise that replacement separately.
<search aria-label="Location search" style="overflow-wrap: anywhere; max-inline-size: 18rem">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-long">Location</label>
<input class="thin-input" id="location-long" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-long-results" aria-describedby="location-long-status" value="">
<ul class="thin-combobox-listbox" id="location-long-results" role="listbox" aria-label="Suggested locations">
<li id="location-long-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-long-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-long-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-long-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<search aria-label="Location search" style="overflow-wrap: anywhere; max-inline-size: 18rem">
<form action="/locations" method="get" style="display: grid; gap: 1rem; padding-block-end: 12rem">
<div class="thin-combobox" data-enhance="combobox">
<label for="location-long">Location</label>
<input class="thin-input" id="location-long" name="location" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-controls="location-long-results" aria-describedby="location-long-status" value="">
<ul class="thin-combobox-listbox" id="location-long-results" role="listbox" aria-label="Suggested locations">
<li id="location-long-chicago" role="option" data-value="Chicago">Chicago, Illinois</li>
<li id="location-long-unavailable" role="option" aria-disabled="true">Unavailable destination</li>
<li id="location-long-long" role="option" data-value="Neustadt an der Weinstraße">Neustadt an der Weinstraße — ausführliche internationale Ortsbezeichnung</li>
</ul>
<p class="thin-combobox-status" id="location-long-status">Two available suggestions and one unavailable destination. Type any location or choose a suggestion.</p>
</div>
<button class="thin-button" type="submit">Search locations</button>
</form>
</search>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>- Closed — shown above
- Open — shown above
- Active option — shown above
- No results — shown above
- Server error — shown above
- Long option — shown above
- Disabled option — shown above
- Narrow container — shown above
- Reconnected after morph — shown above
Interaction and keyboard behavior
Typing remains native. Down/Up opens and moves the active descendant; Home/End move to bounds; Enter copies the active option into the input; Escape closes. Focus remains on the input, pointer selection is supported, and ordinary form submission remains canonical.
Accessibility
Use the ARIA combobox/listbox/option relationship, keep DOM focus on the input, expose aria-expanded and aria-activedescendant, and provide persistent result or error text. Do not announce on every keystroke unnecessarily. Results must be authored by the server or application; this element owns only mechanical selection.
- Contract
COMBO· APG pattern- Basis
- APG Combobox with a flat listbox popup; current implementation uses aria-activedescendant
- Behavior guarantees
- Keeps DOM focus on the named native input and uses aria-activedescendant for a flat list of direct-child options.
- Generates missing listbox and option IDs, ignores nested options, and skips native and ARIA-disabled options.
- Refreshes after child-list morphs.
- Evidence
tests/unit/behaviors/combobox.test.tstests/unit/library/advanced-component-contracts.test.ts
API
Place a labelled input[role=combobox] and an identified [role=listbox] in a .thin-combobox host with data-enhance=combobox. Options may provide data-value. Replace server results in the document to refresh the observed host.
Behavior tier: dependency-free
Structured host, property, and example tables are not authored for this item yet. Use the summary above and the primary example.
Fallback
Without JavaScript, the labelled named text input submits any typed value and server-rendered suggestions remain readable; provide an ordinary submit button and durable server results page.
Assets and dependencies
/components.css/behaviors.js
Dependencies: None
Source: packages/ui/src/items/combobox/style.ts
Composition
Derived from the examples and style specs, not authored.