Combobox
hc-combobox is an accessible single-select input with a
type-to-filter dropdown. Follows the WAI-ARIA 1.2 combobox
pattern — the
<input> carries role="combobox" and the dropdown is a
<ul role="listbox" popover>. Keyboard navigation uses
aria-activedescendant so the visible highlight moves with the
user’s selection while the actual DOM focus stays on the input.
Same architectural primitives as hc-menu and hc-tooltip:
- HTML
popoverattribute for show / hide + Escape / outside dismiss. - CSS Anchor Positioning for the listbox placement under the input.
installCombobox()wires the filter, keyboard, selection, and ARIA bookkeeping.
Browser baseline
Section titled “Browser baseline”| Primitive | Required version |
|---|---|
HTML popover | Chrome 114, Edge 114, Firefox 125, Safari 17 |
| CSS Anchor Positioning | Chrome 125, Edge 125, Firefox 147, Safari 26 |
Browsers without anchor support fall back to a getBoundingClientRect
positioning hook.
Basic example
Section titled “Basic example”- Japan
- United States
- United Kingdom
- France
- Germany (coming soon)
<div class="hc-combobox"> <input class="hc-combobox__input hc-input" type="text" role="combobox" aria-controls="country-list" aria-label="Country">
<ul class="hc-combobox__listbox" id="country-list" role="listbox"> <li class="hc-combobox__option" role="option" data-value="jp">Japan</li> <li class="hc-combobox__option" role="option" data-value="us">United States</li> <li class="hc-combobox__option" role="option" data-value="gb">United Kingdom</li> <li class="hc-combobox__option" role="option" data-value="fr">France</li> <li class="hc-combobox__option" role="option" data-value="de" aria-disabled="true">Germany</li> </ul></div>import { installCombobox } from '@hypermedia-components/core';installCombobox();What installCombobox() does
Section titled “What installCombobox() does”- ARIA wiring on the input:
aria-haspopup="listbox",aria-autocomplete="list",aria-expanded(toggled with the popover state),aria-controlsif missing,aria-activedescendanttracking the highlighted option. - Auto-set
popover="manual"on the listbox if the author did not supply a value. - Anchor binding via inline
anchor-nameon the input andposition-anchoron the listbox so the popover lands directly under the input. - Filter: each input keystroke hides options whose text does
not contain the typed string (case-insensitive). A
.hc-combobox__empty<li role="presentation">appears when nothing matches. - Keyboard:
↓opens / moves to the next visible option;↑moves to the previous;Home/Endjump to the first / last visible enabled option;Enterselects;Escapecloses the listbox without altering the input value;Tabcloses and yields to the next tab stop.
- Mouse: clicking an option selects it.
aria-disabled="true"options are skipped. - Selection: fills the input value, dispatches a bubbling
hc:comboboxselectevent whosedetailcarries{ value, label, option, input }.
htmx usage
Section titled “htmx usage”A selection fires hc:comboboxselect on the input; wire htmx to
that event to push the choice to the server.
<input class="hc-combobox__input hc-input" type="text" role="combobox" aria-controls="user-list" aria-label="Assignee" data-hx-post="/issues/123/assignee" data-hx-trigger="hc:comboboxselect" data-hx-vals='js:{ value: event.detail.value }'>Remote (async) options
Section titled “Remote (async) options”Add data-remote to the .hc-combobox to let the server filter. The
behavior turns off its client-side filter and surfaces the loading / empty /
error states from the htmx request lifecycle:
<div class="hc-combobox" data-remote> <input class="hc-combobox__input hc-input" type="text" role="combobox" aria-controls="city-list" aria-label="City" data-hx-get="/cities" data-hx-trigger="input changed delay:200ms" data-hx-target="#city-list"> <ul class="hc-combobox__listbox" id="city-list" role="listbox"></ul></div>The server returns just the matching <li role="option"> rows and htmx swaps
them into the listbox. While the request is in flight the behavior shows a
spinner row and sets aria-busy="true"; an empty result shows the “No
matches” marker; a failed response shows an error row. The three messages are
translatable via the i18n catalog
(combobox.loading / combobox.empty / combobox.error) or overridable
per-listbox with data-hc-loading / data-hc-empty / data-hc-error.
Debouncing and cancel-in-flight stay with htmx (delay:200ms on the trigger,
hx-sync) — the behavior never makes the request itself.
Creatable
Section titled “Creatable”Add data-allow-create to let the user pick a value that isn’t in the list.
When the typed text has no exact match, a synthetic “Create …” option
appears at the end of the listbox; choosing it (click or Enter)
commits the raw text and fires hc:comboboxselect with created: true.
<div class="hc-combobox" data-allow-create>…</div>input.addEventListener('hc:comboboxselect', (e) => { // e.detail = { value, label, option, input, created } if (e.detail.created) { // persist the new value (POST it, add it to the list, …) }});The label is translatable via the i18n catalog
(combobox.create, e.g. Create "{value}").
Rich options
Section titled “Rich options”Options can hold any HTML — an icon, a two-line label, a description. Two optional attributes keep filtering and selection clean:
data-label— the value written into the input on select (otherwise the option’s text content, which would include the rich markup’s text).data-search— the text the filter matches against (otherwise the label). Use it to match aliases / keywords that aren’t shown.
<li class="hc-combobox__option" role="option" data-value="jp" data-label="Japan" data-search="japan nippon 日本 jp"> <img src="/flags/jp.svg" alt=""> <strong>Japan</strong> <small>Asia</small></li>The filter hides non-matching options with the hidden attribute. If you give
options a display (flex/grid) for the rich layout, scope it to
:not([hidden]) so it doesn’t override the hide:
.hc-combobox__option:not([hidden]) { display: grid; … }Accessibility
Section titled “Accessibility”- The input is the user’s anchor — focus stays there at all times so
type-ahead and screen-reader announcement track correctly.
aria-activedescendantmoves the highlight without moving DOM focus. - Always give the combobox an accessible name. Wrap with a
<label>, setaria-label, or usearia-labelledby. - The listbox carries
role="listbox"and each childrole="option". Mark unavailable rows witharia-disabled="true"rather thandisabled—<li>is not a focusable form control, sodisabledhas no effect. - The visible focus indicator on the active option uses
--hc-color-focus-ringand stays in sync acrossdata-colorthemes.
Out of scope (MVP)
Section titled “Out of scope (MVP)”The MVP keeps the surface small. The following patterns are deferred:
- Multi-select — tag input with multiple chosen values. Will
ship as a separate
hc-multicombobox. - Built-in debounce / cancel-in-flight — async loading itself is
supported (see Remote (async) options), but
request timing stays with htmx (
delay:on the trigger,hx-sync) rather than being bundled into the behavior. - Strict mode — accepting free text as a value is supported via
data-allow-create; the opposite — clearing the input on close when it doesn’t match an option — is still a future opt-in flag.
Theming tokens
Section titled “Theming tokens”Component tokens (in component.tokens.json):
| Token path | Purpose |
|---|---|
combobox.listbox.bg / fg / border / radius | Listbox surface. |
combobox.listbox.max-height | Cap before the listbox scrolls. |
combobox.listbox.padding-block / min-width / offset | Listbox spacing and anchor offset. |
combobox.option.padding-x / padding-y / font-size / gap | Option layout. |
combobox.option.fg / hover-bg / active-bg / selected-bg / selected-fg / disabled-fg | Option states. |
combobox.empty-fg | ”No matches” placeholder color. |
CSS variables
Section titled “CSS variables”Show the generated CSS variables
--hc-combobox-listbox-bg | -listbox-fg | -listbox-border | -listbox-radius--hc-combobox-listbox-max-height | -listbox-padding-block--hc-combobox-listbox-min-width | -listbox-offset--hc-combobox-option-padding-x | -option-padding-y | -option-font-size--hc-combobox-option-fg | -option-hover-bg | -option-active-bg--hc-combobox-option-selected-bg | -option-selected-fg--hc-combobox-option-disabled-fg--hc-combobox-empty-fg--hc-color-focus-ring (inherited from data-color)