oriUI

Combobox

A filterable single-select listbox — type to filter, navigate with the keyboard, pick one. It is the first component driven end-to-end by @oriui/headless: the state machine + prop-getters own the behavior (open/close, filtering, the active-descendant highlight, selection, and the full WAI-ARIA keyboard), and OriCombobox renders the styled shell on top. Unlike the pure-CSS components, the behavior needs JavaScript — the standalone classes give you the look, and useCombobox gives you the behavior for any UI.

State is expressed on real elements: role="combobox" with aria-expanded / aria-controls / aria-activedescendant on the input, role="listbox" + role="option" + aria-selected on the popup. v-model holds the selected option's value.

Classes

The block is a labeled control wrapping a positioned listbox popup. The input reuses .ori-input__field for its box, so the field look stays in one place.

ClassTypeDescription
ori-comboboxBlockRequired base class (wrapper div).
ori-combobox_* (size)Sizeinherit · xs · sm · md · lg · xl · xxl field height.
ori-color_*Colorprimary · … (focus ring + highlight + selected accent).
ori-combobox__control · __input · __trigger · __clear · __listbox · __option · __emptyPartcontrol wrap / input / chevron / clear / popup / option / no-results.
ori-combobox__label · __required · __hint · __errorPartlabel / required-asterisk / helper / error.
ori-combobox_fluidLayoutfull-width (stretches wrapper to 100 %).
role=combobox · aria-expanded · aria-activedescendant · aria-selected · data-highlightedStatereal ARIA + data attributes, not classes.

À la carte: the classes above ship in @oriui/css/components/combobox.css. The shared .ori-anchored placement primitive is inlined here, so anchored.css needs no separate import. Import a foundation (@oriui/css/base.css or @oriui/css/tokens.css) first — the token utilities (ori-color_*, ori-size-radius_*, …) live there, not in the component file. The full bundle @oriui/css is the default and already carries both; see à-la-carte imports.

Basic

Pass options ({ label, value, disabled? }[]) and bind v-model. Type to filter; the list opens as you type.

Type to filter.

vue
<script setup>
import { ref } from 'vue'
const fruit = ref(null)
const options = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Grape', value: 'grape' },
    { label: 'Mango', value: 'mango' }
]
</script>

<template>
    <OriCombobox
        v-model="fruit"
        :options="options"
        label="Fruit"
        placeholder="Search a fruit…"
        hint="Type to filter."
    />
</template>

Clearable

clearable shows a clear button while there is a selection. Keyboard users clear with Escape when the listbox is closed (the button itself is a pointer affordance) — so the selection is always removable without a mouse.

vue
<OriCombobox v-model="fruit" :options="options" label="Fruit" clearable placeholder="Pick one" />

Sizes

xs → xxl, like the other field controls.

vue
<OriCombobox v-model="a" :options="options" label="Small" size="sm" />
<OriCombobox v-model="b" :options="options" label="Large" size="lg" />

States

disabled blocks the control; error renders a role="alert" message and flips aria-invalid; required adds the asterisk and guards the selection — via aria-required plus custom validity, so typing an unmatched query can't satisfy it and the form is blocked until a real option is chosen. A disabled option is skipped during keyboard navigation and can't be selected.

vue
<OriCombobox v-model="a" :options="options" label="Disabled" disabled />
<OriCombobox v-model="b" :options="options" label="With error" error="Please choose a fruit." required />

Headless (useCombobox)

The behavior ships separately in @oriui/headless, so you can build a fully custom combobox UI on the same state machine + ARIA contract. useCombobox returns ready-to-v-bind prop bags and the visible (filtered) items.

vue
<script setup>
import { useCombobox } from '@oriui/headless/vue'

const { items, inputProps, listboxProps, getOptionProps, getOptionState } = useCombobox(() => ({
    options: [
        { label: 'Apple', value: 'apple' },
        { label: 'Banana', value: 'banana' }
    ]
}))
</script>

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

Options are a MaybeRefOrGetter — pass a getter (as above) or a ref instead of a plain object to react to a changing option list or disabled.

Pass a filter ((item, query) => boolean) to override the default case-insensitive substring match — for fuzzy matching, async results, or server-side filtering.

Accessibility

Implements the WAI-ARIA combobox with listbox popup pattern.

  • The input is role="combobox" with aria-autocomplete="list", aria-expanded, and aria-controls pointing at the listbox; the highlighted option is referenced by aria-activedescendant (focus stays on the input — no roving tabindex needed).
  • Options are role="option" with aria-selected; the popup is role="listbox" labeled by the field label. A disabled option carries aria-disabled and is skipped by navigation.
  • hint / error are wired through aria-describedby (error supersedes hint); an aria-describedby attribute you pass is joined with them. error also sets aria-invalid="true" and role="alert". required sets aria-required and blocks submission until a value is selected.
  • The list dismisses on blur and on Escape; selecting an option returns focus context to the input.
KeyAction
↓ / ↑Open the list and move the highlight (wraps).
Home / EndHighlight the first / last option.
EnterSelect the highlighted option.
EscapeClose the list.
TabMove focus away (closes the list).
TypeFilter the options.

Framework API

Props

PropTypeDefaultDescription
clearablebooleanfalseShow a clear button while there is a selection.
colorThemeColor'primary'Accent for the focus ring, highlight, and selected option.
disabledbooleanfalseDisable the control.
errorstring—Error message (role="alert"); sets aria-invalid="true".
filter(item, query) => booleansubstringOverride the default case-insensitive label filter.
fluidbooleanfalseFull-width.
formstring—Associate the hidden value input with a form by id.
hintstring—Helper text; hidden while error is shown.
idstring—Explicit base id; auto-generated via useId when omitted.
invalidbooleanfalseSet aria-invalid without a message.
labelstring—Visible label, wired to the input via for / id.
labelsPartial<ComboboxLabels>EnglishAccessible names of the buttons: open and close (the trigger), clear. Pass only the ones you translate.
namestring—Submit the selected value under this field name (hidden input).
noResultsTextstring'No results'Shown when the filter matches nothing.
optionsComboboxItem[]required{ label, value, disabled? }[].
placeholderstring—Native placeholder.
radiusRadiusSize'md'Field + popup corner radius.
requiredbooleanfalseAsterisk + aria-required; blocks submit until a value is selected.
sizeActionSize'md'Field height + text scale.

v-model

v-model binds the selected option's value (string | null) via defineModel. Selecting an option emits update:modelValue; clearing emits null.

Slots

SlotScopeDescription
labelnoneRich label content (an icon + text, markup). Falls back to the label prop; keeps the for/id + listbox aria-labelledby wiring.
option{ option, index, selected }Per-option content — rich options such as an avatar + email or a leading icon. Rendered for every option. Falls back to option.label.
emptynoneNo-results content, shown when the filter matches nothing. Falls back to the noResultsText prop.