HeadlessCombobox

headless
github.com/pdanpdan/headless-components/packages/headless-combobox

An accessible, renderless combobox. It renders nothing on its own — the default scoped slot exposes state, ARIA prop bags, and actions so you own 100% of the markup and styling. Supports single or multiple selection (with min/max counts) and reactive validation.

Installation

pnpm add @pdanpdan/headless-combobox

Examples

Each popup uses the native Popover API (top-layer, via popover="manual") with the exposed popupStyle for anchor positioning. The keyboard-active and mouse-hovered option share the same highlight (via setHighlightedIndex on hover); the selection is shown separately with a check.

Text options · single · no filter

A list of plain string options, single selection, no search input.

Framework (single, no filter)

Object options · single · with filter

Object options with a custom layout and a searchable input.

Assign user (single, with filter)

Text options · multiple · with filter

Multiple selection capped with max; selecting toggles, dropdown stays open.

Frameworks (multiple, max 3)

Text options · multiple · removable chips

Selected items as removable chips. The remove buttons live outside the combobox trigger, so the trigger stays a single focusable control.

Frameworks (multiple, max 3)
Vue Svelte

Text options · multiple · custom options

Options are fully customizable: the focused option gets a thick left border, and each option shows a checkbox (checked / unchecked) for its selection state.

Frameworks (multiple, custom options)

Object options · multiple · validated

Multiple selection with required + min/max validation and a rendered message.

Reviewers (multiple, 2–4 required)

Select at least 2 options.

Select alignment

No search input; the dropdown aligns so the selected option overlays the trigger.

Assign user

Typeahead (editable combobox)

Canonical APG editable pattern: the text input itself is the combobox (role=combobox); type to filter.

Language (typeahead, editable)

Typeahead · chips inside the field

GitHub-style topic input: chips and the text input share one bordered field. The field is a plain container, so each chip can carry a remove button without nesting controls.

Topics (typeahead, removable chips in the field)
TypeScript

API

Props

NameTypeDefaultDescription
modelValueT | T[] | nullSelected option(s) (v-model). Array in multiple mode.
optionsT[]List of options.
multiplebooleanfalseEnable multiple selection.
minLengthnumberMultiple: minimum number of selected options (validation).
maxLengthnumberMultiple: maximum selected. Blocks adding beyond it.
requiredbooleanfalseRequire a selection (single: a value; multiple: >= 1).
disabledbooleanfalseDisable the control: not focusable, cannot open or change.
readonlybooleanfalseRead-only: focusable, shows the value, but cannot open or change.
closeOnSelectboolean | null!multipleClose the dropdown after selecting.
displayValue(option: T) => stringString(option)Maps an option to a string for filtering / rendering.
filterFn(option: T, query: Q) => booleansubstringCustom filter function. Q defaults to string.
idstringuseId()Base id for accessibility attributes.
alignSelectedbooleanfalseAlign the dropdown so the selected option covers the trigger.
errorMessagesPartial<Record<HeadlessComboboxErrorCode, string>>Override default validation messages.

Emits

NameTypeDescription
update:modelValueT | T[] | nullEmitted when the selection changes.

Slot default

Renderless. Receives the scope below to build the entire UI.

State

Reactive values describing the current state.

NameTypeDescription
isOpenbooleanWhether the dropdown is open.
multiplebooleanWhether multiple selection is enabled.
disabledbooleanWhether the control is disabled.
readonlybooleanWhether the control is read-only.
searchQueryQ | undefinedCurrent search query. Q defaults to string.
filteredOptionsT[]Options after applying the filter.
highlightedIndexnumberIndex of the highlighted option (-1 when none).
alignmentOffsetnumberPixel offset used for alignment.
cssAnchorNamestringUnique CSS anchor-name for popover positioning.
popupStyleHeadlessComboboxPopupStyleDefault popup positioning style — spread/merge onto the dropdown.
selectedCountnumberNumber of selected options.
canSelectMorebooleanFalse when at `maxLength` (multiple).
isSelected(option: T) => booleanWhether an option is selected.
validbooleanWhether the current selection passes validation.
errorsHeadlessComboboxErrorCode[]Active validation errors ('required' | 'minlength' | 'maxlength').
validationMessagestringHuman-readable message for the first error.

ARIA prop bags

Spread onto elements with v-bind for accessibility.

NameTypeDescription
triggerPropsHeadlessComboboxTriggerPropsAttributes + toggle/keydown handlers for the trigger (role=combobox, aria-expanded, aria-activedescendant, …).
inputPropsHeadlessComboboxInputPropsAttributes + input/keydown handlers for an in-popup search/filter input (role=searchbox, aria-activedescendant, …).
comboboxInputPropsHeadlessComboboxComboboxInputPropsAttributes + open/focus/input handlers for a typeahead input that is itself the combobox (role=combobox, aria-expanded, aria-autocomplete, …).
listboxPropsHeadlessComboboxListboxPropsAttributes for the listbox (role, aria-multiselectable).
getOptionProps(option, index) => HeadlessComboboxOptionPropsAttributes + select/mouse/focus handlers for an option (role, aria-selected, aria-disabled, …).

Actions

Methods to drive the combobox.

NameTypeDescription
toggle() => voidToggle the dropdown open/closed.
open() => voidOpen the dropdown.
close(returnFocus?: boolean) => voidClose the dropdown.
select(option: T) => voidSelect (single) or toggle (multiple) an option.
clear() => voidClear the selection (null or []).
setSearchQuery(value: Q | undefined) => voidUpdate the search query.
setHighlightedIndex(index: number) => voidSet the highlighted option — wire to hover to match keyboard.
focusInput() => voidFocus the filter input (keeps focus while the popup stays open).
handleKeydown(event: KeyboardEvent) => voidKeyboard navigation handler.

Ref setters

Assign to elements with :ref to wire up focus and positioning.

NameTypeDescription
setContainerRefref fnThe root container element.
setTriggerRefref fnThe trigger button element.
setDropdownRefref fnThe dropdown popup element.
setInputRefref fnThe search input element.
setListRefref fnThe options list element.
setOptionRef(option, el) => voidEach option element.