oriUI

useCombobox

A headless combobox primitive — an editable text input paired with a popup listbox — implementing the WAI-ARIA combobox with listbox popup pattern: type to filter, navigate with the keyboard via active-descendant, and pick a single option. It owns the behavior (open/close, filtering, the highlight, selection, and the full keyboard) and the ARIA wiring as ready-to-bind prop bags; you own the markup and styles.

This is the Vue binding; the framework-agnostic core lives in @oriui/headless, and the styled OriCombobox is built on it. useDialog and useDisclosure lean on a native platform element; a combobox has no native primitive, so useCombobox's default is the core state machine + prop-getters. Like the overlays it still resolves through the OriHeadless contract, so an app can swap a custom / Zag-backed engine — the native core adapter is simply the default. It is the first behavior to fully exercise the @oriui/headless engine.

Import

ts
import { useCombobox } from '@oriui/headless/vue'

Options

Pass an options object — or a getter returning one, to stay reactive (the styled component passes () => ({ … })). options, disabled, and filter are read live; id, defaultValue and defaultInputValue are read once at setup.

OptionTypeDefaultDescription
optionsComboboxItem[]requiredThe full option list. Reactive — filtering re-runs when it changes.
idstringauto (useId)Stable base id for the part ARIA wiring; auto-generated when omitted (pass one for stable SSR).
defaultValuestring | nullnullThe selected value to start with (read once).
defaultInputValuestring''The input text to start with (read once).
disabledbooleanfalseDisable the control; also closes the listbox.
filter(item: ComboboxItem, query: string) => booleansubstringFilter predicate; default = case-insensitive substring on the label. Swap for fuzzy / async.
labelsPartial<ComboboxLabels>EnglishAccessible names of the trigger (open, close) and the clear button (clear). Read live.

Each option is a ComboboxItem — value is the stable identity, label is what the input shows:

ts
interface ComboboxItem {
    value: string
    label: string
    disabled?: boolean
}

Returns

A control object — the reactive state plus the prop bags you bind to each part. Every *Props value is a ComputedRef; bind it with v-bind. The get* / set* and the action members are plain functions.

PropertyTypeDescription
openComputedRef<boolean>Whether the listbox is open.
valueComputedRef<string | null>The selected option's value, or null.
inputValueComputedRef<string>The current input text.
highlightedValueComputedRef<string | null>The active (keyboard-highlighted) option's value, or null.
itemsComputedRef<ComboboxItem[]>The visible (filtered) options — render these, and pass their index into getOptionProps.
rootPropsComputedRef<object>The wrapper element (data-state).
labelPropsComputedRef<object>The <label> wiring (id / for → the input).
controlPropsComputedRef<object>The control wrapper around input + triggers (data-state / data-disabled).
inputPropsComputedRef<object>The role="combobox" input: aria-autocomplete / aria-expanded / aria-controls / aria-activedescendant plus the input & keydown handlers.
triggerPropsComputedRef<object>The open/close toggle button (tabindex="-1", aria-expanded / aria-controls).
clearTriggerPropsComputedRef<object>The clear button (tabindex="-1").
listboxPropsComputedRef<object>The role="listbox" popup (aria-labelledby, hidden while closed).
getOptionProps(item, index)(item: ComboboxItem, index: number) => objectPer-option prop bag (role="option", aria-selected, data-highlighted, click / pointermove). index must be the position in items.
getOptionState(item)(item: ComboboxItem) => { highlighted: boolean; selected: boolean }The { highlighted, selected } state for the item — for conditional classes.
setOpen(open)(open: boolean) => voidOpen / close the listbox imperatively.
setInputValue(next)(next: string) => voidSet the input text (filters and opens).
select(item)(item: ComboboxItem) => voidCommit a selection — reflects its label into the input and closes.
clear()() => voidClear the selection and the input.

Usage

Bind each part's prop bag with v-bind, render items, and pass the same index you iterate with into getOptionProps:

vue
<script setup lang="ts">
import { useCombobox } from '@oriui/headless/vue'

const cb = useCombobox(() => ({
    options: [
        { label: 'Apple', value: 'apple' },
        { label: 'Banana', value: 'banana' },
        { label: 'Cherry', value: 'cherry' }
    ]
}))
</script>

<template>
    <div v-bind="cb.rootProps.value">
        <label v-bind="cb.labelProps.value">Fruit</label>

        <div v-bind="cb.controlProps.value">
            <input v-bind="cb.inputProps.value" placeholder="Search a fruit…" @blur="cb.setOpen(false)" />
            <button v-bind="cb.triggerProps.value">▾</button>

            <ul v-bind="cb.listboxProps.value">
                <li
                    v-for="(item, index) in cb.items.value"
                    :key="item.value"
                    v-bind="cb.getOptionProps(item, index)"
                    :data-selected="cb.getOptionState(item).selected"
                >
                    {{ item.label }}
                </li>
            </ul>
        </div>
    </div>
</template>

The machine keeps no notion of validation, v-model, or blur-to-close — those live in the consumer. The styled OriCombobox wraps exactly this pattern and layers the form contract (label / hint / error / required), v-model sync, and dismissal on top: it closes the list on a press or a focus outside with useDismissable.

Accessibility

Implements the WAI-ARIA combobox with listbox popup pattern; the prop bags carry the wiring.

  • inputProps set role="combobox" with aria-autocomplete="list", autocomplete="off", aria-expanded, and aria-controls pointing at the listbox; the highlighted option is referenced by aria-activedescendant — focus never leaves the input, so no roving tabindex is needed.
  • listboxProps set role="listbox", aria-labelledby (the element you bind labelProps to), and hidden while closed. Options from getOptionProps are role="option" with aria-selected; a disabled item carries aria-disabled and is skipped by keyboard navigation. Each option also exposes data-highlighted (active) and data-state="checked|unchecked" for styling.
  • triggerProps / clearTriggerProps are tabindex="-1" buttons (the input keeps focus) with accessible labels ("Open / Close suggestions", "Clear selection"); the trigger also carries aria-expanded / aria-controls.
  • Selecting an option reflects its label into the input and closes the list. The machine does not dismiss on blur or on a press outside — wire that yourself, with setOpen(false) on the input's blur (as the example does) or with useDismissable (as the styled component does); Tab therefore moves focus away but does not itself close the list.
  • Option ids are derived from the position in items, so always pass the same index you iterate items with into getOptionProps(item, index), or aria-activedescendant will point at the wrong row.
KeyAction
↓ / ↑Open the list (if closed) and move the highlight to the next / previous enabled option (wraps).
Home / EndHighlight the first / last enabled option (when open).
EnterSelect the highlighted option (when open).
EscapeClose the list (when open).
TypeFilter the options and (re)open the list.

See also