Demo
Searchable listbox input with keyboard navigation and optional multiple selection.
No results.
Alpine.js Combobox
huxCombobox provides combobox behavior for Alpine.js with filtered option rendering, keyboard navigation, and optional multiple selection. Use it when you need behavior-only combobox logic that you can pair with your own markup and styles.
API
huxCombobox(config)
Returns an Alpine data object with:
isOpen: booleansearchQuery: stringselectedValue: stringselectedValues: string[]comboboxInputId: string | nulllistboxId: string | nullfilteredOptionItems: Array<{ optionItem: { label: string, value: string }, sourceIndex: number }>activeDescendantId: string | nullopenList(): voidcloseList(): voidonInputChange(): voidfocusNextOptionItem(): voidfocusPreviousOptionItem(): voidselectFocusedOptionItem(): voidtoggleOptionItem(optionItem): voidclearSelection(): void
Internal helper methods are private implementation details and are not part of the supported API contract.
Options
optionItems: Array<{ label: string, value: string }>(default:[])acceptMultiple: boolean(default:false)clearSelectionOnInputClear: boolean(default:false)hideOptionItemsWhenSearchQueryEmpty: boolean(default:false)inputTemplateRef: string(default:'comboboxInput')listboxTemplateRef: string(default:'comboboxListbox')
Quick Start
<div
x-data="huxCombobox({
optionItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' }
]
})"
x-on:click.outside="closeList()"
>
<label x-bind:for="comboboxInputId">Fruit</label>
<input
x-ref="comboboxInput"
x-model="searchQuery"
x-bind:id="comboboxInputId"
x-bind:aria-expanded="isOpen.toString()"
x-bind:aria-controls="listboxId"
role="combobox"
aria-autocomplete="list"
aria-haspopup="listbox"
x-on:input="onInputChange()"
x-on:keydown.down.prevent="focusNextOptionItem()"
x-on:keydown.up.prevent="focusPreviousOptionItem()"
x-on:keydown.enter.prevent="selectFocusedOptionItem()"
x-on:keydown.escape.stop.prevent="closeList()"
/>
<div
x-ref="comboboxListbox"
x-bind:id="listboxId"
role="listbox"
aria-label="Fruit options"
x-bind:aria-multiselectable="acceptMultiple ? 'true' : null"
x-cloak
x-show="isOpen"
>
<template x-for="({ optionItem, sourceIndex }) in filteredOptionItems" x-bind:key="sourceIndex">
<button
type="button"
role="option"
tabindex="-1"
x-bind:id="optionItemId(sourceIndex)"
x-on:click="toggleOptionItem(optionItem)"
x-text="optionItem.label"
></button>
</template>
</div>
</div>Common Usage Patterns
Multiple Selection
huxCombobox({
optionItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Apricot', value: 'apricot' },
],
acceptMultiple: true,
})Keep List Closed Until Search Starts
huxCombobox({
optionItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
],
hideOptionItemsWhenSearchQueryEmpty: true,
})Behavior Contract
optionItemsare normalized to an array before filtering.filteredOptionItemsmatches options by case-insensitive label includes.onInputChange()opens the list and resets focused option to-1.clearSelectionOnInputClearonly clears for single-select mode.- In single-select mode, selecting an option sets
selectedValue, mirrors label intosearchQuery, and closes the list. - In multi-select mode,
toggleOptionItem()mutatesselectedValuesand keeps or closes the list depending onhideOptionItemsWhenSearchQueryEmpty. activeDescendantIdisnullunless the list is open and a focused option exists.- Arrow-key navigation scrolls the active option into view (
scrollIntoView({ block: 'nearest' })) so it stays visible in a scrollable listbox.
Error Handling
- Missing input/listbox refs do not throw. The component logs a
[huxCombobox]missing-ref error and falls back to generated ids. - Unknown or malformed option data is tolerated by filtering and string conversion behavior; operations no-op when target options are unavailable.
Accessibility Notes
- Keep the input wired with
role="combobox",aria-autocomplete="list", andaria-controlspointing at the listbox id. - Give the listbox an accessible name (
aria-label) and, in multi-select mode, setaria-multiselectable="true"so assistive technology announces that more than one option can be chosen. - Keep option elements out of the tab sequence with
tabindex="-1"— thearia-activedescendantpattern keeps real focus on the input while arrow keys move the active option. - Keep options keyboard reachable via arrow keys and Enter bindings (
focusNextOptionItem(),focusPreviousOptionItem(),selectFocusedOptionItem()). - Ensure option row active/selected states use visible contrast and not color alone.
- Keep remove/clear controls as
buttonelements withtype="button"and clear labels (aria-label).
Notes
- The component generates ids when refs have no
id, so explicit ids are optional.