Skip to content

Combobox

A multi-select, filterable text input that follows the WAI-ARIA APG combobox (list-autocomplete) pattern, with each pick rendered as a dismissible chip.

Import

import { Combobox } from "@hyzer-labs/ui"

Demo

Select vs Combobox

Use Combobox when there are many options, where filtering or virtualization helps, or when you need type-to-filter search. For a small, static option set, prefer the simpler native Select, including its own native multiple.

Type to filter, or open with the toggle/ArrowDown. Clicking an option (or pressing Enter on it) toggles its membership. The popup stays open so you can keep picking, and each pick becomes a dismissible chip. Backspace on an empty query removes the last chip. A disabled option (Zone) stays visible but cannot be toggled. Its chip is pre-selected here to show that an existing selection stays removable even when the option itself is inert.

Aviar Zone (out of stock)
  • Aviar
  • Luna
  • Judge
  • Zone (out of stock)
  • Punisher
const putters: FormOption[] = [
	{ value: 'aviar', label: 'Aviar' },
	{ value: 'luna', label: 'Luna' },
	{ value: 'judge', label: 'Judge' },
	{ value: 'zone', label: 'Zone (out of stock)', disabled: true },
	{ value: 'punisher', label: 'Punisher' }
];

<Combobox name="putters" label="Putters" options={putters} bind:value={bag} />

Props

NameTypeDefaultNote
namestringRequired. Repeated on every hidden input.
labelstringRequired. Always in the DOM; sr-only with hideLabel.
optionsFormOption[]Required. Flat, multi-select — see FormOption below.
valuestring[][]Bindable. Selected option values, in selection order.
placeholderstring'Search...'Shown only while nothing is selected — once chips exist, they say it better.
filter(query: string, option: FormOption) => booleanOverrides the default case-insensitive substring match.
emptyTextstring'No results'
toggleLabelstring'Show options'
chipPropsComboboxChipProps{}Styling applied to every chip — see ComboboxChipProps below. Resolves to size: 'sm'.
onchange(value: string[]) => voidFires on every selection change: toggle, chip dismiss, or Backspace removal.
descriptionstringHelp text below the label.
errorstringInline error message; sets the error state.
requiredbooleanfalse
disabledbooleanfalse
hideLabelbooleanfalse
classstringMerged after the hz-field hz-combobox classes.

FormOption

NameTypeDefaultNote
valuestringRequired.
labelstringRequired. Shown on the chip and in the option row.
disabledbooleanInert — visible but never toggleable; an existing selection stays removable.

ComboboxChipProps

NameTypeDefaultNote
intentIntent'neutral'See Foundation → Colors & Intent.
variant'soft' | 'solid' | 'outline''soft'
size'sm' | 'md''sm'Combobox's own default overrides Badge's md default.
rounded'none' | 'sm' | 'md' | 'lg' | 'full''full'
classstring

Theme hooks

What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.

Root class: .hz-field hz-combobox

Data attributes

HookValuesStyles
data-state'default' | 'error' | 'disabled'The universal field state hook, on the root. Precedence is fixed: error beats disabled.
data-openpresent while the popup is openOn the root. Rotates the toggle chevron and gates the popup.
data-activepresent on the active optionVirtual focus — the highlighted option. Real focus stays in the input.
data-selectedpresent on selected optionsSelected options; the theme adds a checkmark.
data-disabledpresent on disabled optionsPer-option. Disabled options stay focusable (aria-disabled, not the native attribute).

Part classes

HookValuesStyles
.hz-field-labelchild elementThe label.
.hz-field-requiredchild elementThe required marker.
.hz-field-descriptionchild elementThe hint text.
.hz-field-errorchild elementThe error message.
.hz-combobox-controlchild elementThe bordered row holding the input, chips, and toggle.
.hz-combobox-inputchild elementThe text input.
.hz-combobox-togglechild elementThe chevron button.
.hz-combobox-popupchild elementThe floating panel.
.hz-combobox-listboxchild elementThe option list.
.hz-combobox-optionchild elementOne option.
.hz-combobox-emptychild elementThe no-results message.

Accessibility

The visible input carries role=combobox with aria-autocomplete=list, aria-expanded, and aria-controls pointing at the role=listbox popup, which is aria-multiselectable. DOM focus never moves into the list: the highlighted option is tracked as virtual focus with aria-activedescendant on the input, so screen readers announce the active option while the field stays editable.

description/error chain into aria-describedby on the input (description first), still computed even while the popup visually overlays that region. required sets aria-required, and an error sets aria-invalid.

Each selected value renders as a Badge chip inside the control. Every chip has a labeled dismiss button, announced as Remove followed by the option's label. Each of those buttons is a real tab stop of its own, so tabbing into the field reaches the chips before the input.

Keyboard: ArrowDown/ArrowUp open the popup and move the active option, wrapping and skipping disabled entries. Alt+ArrowDown opens without moving, and Alt+ArrowUp closes without changing anything. Home/End jump to the first and last enabled option while open (they're native text-cursor moves while closed). Enter toggles the active option's membership and keeps the popup open so you can keep picking. Backspace on an empty query removes the last chip. Escape closes the popup and clears the filter query. It never clears a selection, since every chip has its own dismiss button. Tab closes the popup and lets focus move on.

The input is the only tab stop that opens the popup. The trailing toggle button is tabindex=-1, reachable by pointer only.

References: APG Combobox pattern