ComboBox

A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query

Usage

<script setup lang="ts">
import { ComboBox } from '@hareui/vue'
</script>
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox class="w-[256px]" label="Favorite Animal" placeholder="Search animals..." :items="animals" /></template>

Anatomy

ComboBox renders a Label, an input group (an Input and a trigger button), a popover holding a ListBox-styled option list, and an optional Description / FieldError. Options use ListBox's items format, including sections and separators.

<template>
  <ComboBox label="Animal" :items="items">
    <template #label />                                  <!-- Label content -->
    <template #indicator="{ open }" />                   <!-- trigger button content (default: chevron) -->
    <!-- Displays the selected values, primarily used for multiple selection -->
    <template #value="{ selectedItems, isPlaceholder, text }" />
    <template #description />                            <!-- Description content -->
    <template #error="validation" />                     <!-- FieldError content -->
    <template #item-leading="{ item, selected }" />      <!-- ListBox item slots -->
    <template #item-indicator="{ selected }" />          <!-- option checkmark -->
    <template #section-label="{ section }" />            <!-- section header -->
    <template #empty />                                  <!-- with `allows-empty-collection` -->
    <template #loading />                                <!-- with `loading`: after the last option -->
  </ComboBox>
</template>
ui keydata-slotElement
basecombo-boxroot <div>
labellabelLabel
inputGroupcombo-box-input-group<div role="group"> around the input and trigger
inputinput<input role="combobox"> (Input styles)
triggercombo-box-trigger<button> opening the popover
valuecombo-box-valueselected values (multiple mode or #value)
popovercombo-box-popoverpopover holding the list

Typing items

items takes the exported ListBoxItem type (ListBoxEntry when it mixes in sections and separators). Extra fields are kept: extend the type, and the slots receive your type, so no casts are needed.

import type { ListBoxItem } from '@hareui/vue'

interface MyItem extends ListBoxItem {
  icon: string
}

const items: MyItem[] = [/* ... */]
// <template #item-leading="{ item }"> — item.icon is a string

Examples

Full Width

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },]</script><template>  <div class="w-[400px] space-y-4">    <ComboBox full-width label="Favorite Animal" placeholder="Search animals..." :items="animals" />  </div></template>

With Description

Search and select your favorite animal
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox class="w-[256px]" label="Favorite Animal" placeholder="Search animals..." description="Search and select your favorite animal" :items="animals" /></template>

Required

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, ComboBox, Form } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]function onSubmit(event: SubmitEvent) {  const data = Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement))  console.log('Form submitted:', data)  alert('Form submitted successfully!')}</script><template>  <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="onSubmit">    <ComboBox required class="w-full" name="animal" label="Favorite Animal" placeholder="Search animals..." :items="animals" />    <Button type="submit">      Submit    </Button>  </Form></template>

Disabled

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox disabled class="w-[256px]" label="Favorite Animal" placeholder="Search animals..." default-value="cat" :items="animals" /></template>

With Disabled Options

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'dog', label: 'Dog' },  { value: 'cat', label: 'Cat', disabled: true },  { value: 'bird', label: 'Bird' },  { value: 'kangaroo', label: 'Kangaroo', disabled: true },  { value: 'elephant', label: 'Elephant' },  { value: 'tiger', label: 'Tiger' },]</script><template>  <ComboBox class="w-[256px]" label="Animal" placeholder="Search animals..." :items="animals" /></template>

With Sections

<script setup lang="ts">import type { ListBoxEntry } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const countries: ListBoxEntry[] = [  {    type: 'section',    label: 'North America',    items: [      { value: 'usa', label: 'United States' },      { value: 'canada', label: 'Canada' },      { value: 'mexico', label: 'Mexico' },    ],  },  { type: 'separator' },  {    type: 'section',    label: 'Europe',    items: [      { value: 'uk', label: 'United Kingdom' },      { value: 'france', label: 'France' },      { value: 'germany', label: 'Germany' },      { value: 'spain', label: 'Spain' },      { value: 'italy', label: 'Italy' },    ],  },  { type: 'separator' },  {    type: 'section',    label: 'Asia',    items: [      { value: 'japan', label: 'Japan' },      { value: 'china', label: 'China' },      { value: 'india', label: 'India' },      { value: 'south-korea', label: 'South Korea' },    ],  },]</script><template>  <ComboBox class="w-[256px]" label="Country" placeholder="Search countries..." :items="countries" /></template>

Controlled

Selected: Cat

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'import { computed, ref } from 'vue'const animals: ListBoxItem[] = [  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'bird', label: 'Bird' },  { value: 'fish', label: 'Fish' },  { value: 'hamster', label: 'Hamster' },]const selectedKey = ref<string | null>('cat')const selectedAnimal = computed(() => animals.find(a => a.value === selectedKey.value))</script><template>  <div class="space-y-2">    <ComboBox v-model="selectedKey" class="w-[256px]" label="Animal (controlled)" placeholder="Search animals..." :items="animals" />    <p class="text-sm text-muted">      Selected: {{ selectedAnimal?.label || 'None' }}    </p>  </div></template>

Controlled Input Value

Bind v-model:input-value to read or set the text in the input.

Input value: (empty)

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'import { ref } from 'vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]const inputValue = ref('')</script><template>  <div class="space-y-2">    <ComboBox v-model:input-value="inputValue" class="w-[256px]" label="Search (controlled input)" placeholder="Type to search..." :items="animals" />    <p class="text-sm text-muted">      Input value: {{ inputValue || '(empty)' }}    </p>  </div></template>

Asynchronous Loading

Bind v-model:input-value to refetch as the user types and set :filter="false" so the fetched items are shown as they are. Bind loading and listen to load-more: it fires whenever the end of the list scrolls into view while loading is false. The #loading slot shows after the last option while loading.

<script setup lang="ts">import { ComboBox, EmptyState, Spinner } from '@hareui/vue'import { onMounted, ref, watch } from 'vue'interface Character {  name: string}// HeroUI's `useAsyncList` equivalent: refetch on every input change, follow the `next` cursor on `load-more`const filterText = ref('')const characters = ref<Character[]>([])const loading = ref(false)let cursor: string | null = nulllet controller: AbortController | undefinedasync function load(url: string, append: boolean) {  controller?.abort()  controller = new AbortController()  loading.value = true  try {    const json = await (await fetch(url.replace(/^http:\/\//i, 'https://'), { signal: controller.signal })).json()    cursor = json.next    characters.value = append ? [...characters.value, ...json.results] : json.results    loading.value = false  }  catch (error) {    if ((error as Error).name !== 'AbortError')      loading.value = false  }}function loadMore() {  if (cursor && !loading.value)    load(cursor, true)}watch(filterText, text => load(`https://swapi.py4e.com/api/people/?search=${text}`, false))// Client only: VitePress SSR-renders demosonMounted(() => load('https://swapi.py4e.com/api/people/?search=', false))</script><template>  <ComboBox    v-model:input-value="filterText"    allows-empty-collection    class="w-[256px]"    label="Pick a Character"    placeholder="Star Wars characters..."    :filter="false"    :items="characters.map(c => ({ value: c.name, label: c.name }))"    :loading="loading"    @load-more="loadMore"  >    <template #empty>      <EmptyState />    </template>    <template #loading>      <div class="flex items-center justify-center gap-2 py-2">        <Spinner size="sm" />        <span class="muted text-sm">Loading more...</span>      </div>    </template>  </ComboBox></template>

Default Selected Key

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox class="w-[256px]" label="Favorite Animal" placeholder="Search animals..." default-value="cat" :items="animals" /></template>

Allows Custom Value

You can type any animal name, even if it's not in the list
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox allows-custom-value class="w-[256px]" label="Favorite Animal" placeholder="Search or type an animal..." description="You can type any animal name, even if it's not in the list" :items="animals" /></template>

Custom Indicator

<script setup lang="ts">import { Icon } from '@iconify/vue'import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox class="w-[256px]" label="Favorite Animal" placeholder="Search animals..." :items="animals">    <template #indicator>      <Icon class="size-3" icon="gravity-ui:chevrons-expand-vertical" />    </template>  </ComboBox></template>

Custom Value

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Avatar, ComboBox } from '@hareui/vue'interface User extends ListBoxItem {  avatarUrl: string  fallback: string}const users: User[] = [  { value: '1', label: 'Bob', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg', fallback: 'B' },  { value: '2', label: 'Fred', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg', fallback: 'F' },  { value: '3', label: 'Martha', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg', fallback: 'M' },  { value: '4', label: 'John', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg', fallback: 'J' },  { value: '5', label: 'Jane', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg', fallback: 'J' },]</script><template>  <ComboBox class="w-[256px]" label="User" placeholder="Search users..." :items="users">    <template #item-leading="{ item }">      <Avatar size="sm" :src="item.avatarUrl" :fallback="item.fallback" />    </template>  </ComboBox></template>

Custom Filtering

Pass filter to replace the default case- and accent-insensitive "contains" match.

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'bird', label: 'Bird' },  { value: 'fish', label: 'Fish' },  { value: 'hamster', label: 'Hamster' },]// Plain case-insensitive substring match (the default also ignores accents)function filter(text: string, inputValue: string) {  if (!inputValue)    return true  return text.toLowerCase().includes(inputValue.toLowerCase())}</script><template>  <ComboBox class="w-[256px]" label="Animal (custom filter)" placeholder="Search animals..." :filter="filter" :items="animals" /></template>

Render Function

HeroUI's render prop has no HareUI equivalent; attributes such as data-custom fall through to the root.

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <!-- HeroUI's `render` prop has no Vue equivalent (plan drop: render functions); attributes fall through to the root instead. -->  <ComboBox class="w-[256px]" data-custom="foo" label="Favorite Animal" placeholder="Search animals..." :items="animals" /></template>

Use the menu-trigger prop to control when the popover opens:

  • focus (default): popover opens when the user focuses the input
  • input: popover opens when the user edits the input text
  • manual: popover only opens when the user presses the trigger button or uses the arrow keys

Focus (default)

Popover opens when the input is focused

Input

Popover opens when the user edits the input text

Manual

Popover only opens when the trigger button is pressed or arrow keys are used
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <div class="flex flex-col gap-8">    <div class="flex flex-col gap-2">      <p class="text-sm font-medium text-muted">        Focus (default)      </p>      <ComboBox class="w-[256px]" menu-trigger="focus" label="Favorite Animal" placeholder="Search animals..." description="Popover opens when the input is focused" :items="animals" />    </div>    <div class="flex flex-col gap-2">      <p class="text-sm font-medium text-muted">        Input      </p>      <ComboBox class="w-[256px]" menu-trigger="input" label="Favorite Animal" placeholder="Search animals..." description="Popover opens when the user edits the input text" :items="animals" />    </div>    <div class="flex flex-col gap-2">      <p class="text-sm font-medium text-muted">        Manual      </p>      <ComboBox class="w-[256px]" menu-trigger="manual" label="Favorite Animal" placeholder="Search animals..." description="Popover only opens when the trigger button is pressed or arrow keys are used" :items="animals" />    </div>  </div></template>

Form Value

Use the form-value prop to control whether the selected item's value or text is submitted in forms. key is the default. When allows-custom-value is set, the text is always submitted.

Submits the selected key
Submits the selected text
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, ComboBox, Form } from '@hareui/vue'import { ref } from 'vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]const message = ref<string | null>(null)function onSubmit(event: SubmitEvent) {  const formData = new FormData(event.currentTarget as HTMLFormElement)  message.value = `Key: ${formData.get('animal')}. Text: ${formData.get('animal-text')}.`}</script><template>  <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="onSubmit">    <ComboBox required class="w-full" form-value="key" name="animal" label="Animal" placeholder="Select an animal..." description="Submits the selected key" :items="animals" />    <ComboBox required class="w-full" form-value="text" name="animal-text" label="Animal (text)" placeholder="Select an animal..." description="Submits the selected text" :items="animals" />    <Button class="w-fit" type="submit">      Submit    </Button>    <p v-if="message" class="text-sm text-muted">      {{ message }}    </p>  </Form></template>

Validation Behavior

Use the validation-behavior prop to control how validation is displayed:

  • native (default): blocks form submission when the value is missing or invalid
  • aria: shows errors in realtime and does not block submission
Blocks submission when the value is missing
Shows errors in realtime and does not block submission
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, ComboBox, Form } from '@hareui/vue'import { ref } from 'vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]const nativeMessage = ref<string | null>(null)const ariaMessage = ref<string | null>(null)</script><template>  <div class="flex flex-col gap-8">    <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="nativeMessage = 'Submitted with native validation.'">      <ComboBox required class="w-full" name="animal" validation-behavior="native" label="Animal (native)" placeholder="Select an animal..." description="Blocks submission when the value is missing" :items="animals" />      <Button class="w-fit" type="submit">        Submit      </Button>      <p v-if="nativeMessage" class="text-sm text-muted">        {{ nativeMessage }}      </p>    </Form>    <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="ariaMessage = 'Submitted with ARIA validation.'">      <ComboBox required class="w-full" name="animal-aria" validation-behavior="aria" label="Animal (ARIA)" placeholder="Select an animal..." description="Shows errors in realtime and does not block submission" :items="animals" />      <Button class="w-fit" type="submit">        Submit      </Button>      <p v-if="ariaMessage" class="text-sm text-muted">        {{ ariaMessage }}      </p>    </Form>  </div></template>

Custom Validation

Use the validate prop to return an error message when the value is invalid, or true when it is valid. It receives { inputValue, value, selectedKey }.

<script setup lang="ts">import type { ComboBoxValidationValue, ListBoxItem } from '@hareui/vue'import { Button, ComboBox, Form } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]function validate(value: ComboBoxValidationValue) {  if (value.selectedKey == null)    return 'Please select an animal'  if (value.selectedKey === 'snake')    return 'Snakes are not allowed'  return true}function onSubmit() {  alert('Form submitted successfully!')}</script><template>  <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="onSubmit">    <ComboBox required class="w-full" name="animal" label="Favorite Animal" placeholder="Search animals..." :validate="validate" :items="animals" />    <Button class="w-fit" type="submit">      Submit    </Button>  </Form></template>

Read Only

Use the readonly prop to make the ComboBox read-only. The selected value can be focused, but it cannot be changed.

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox readonly class="w-[256px]" label="Favorite Animal" placeholder="Search animals..." default-value="cat" :items="animals" /></template>

Multiple Selection

Set selection-mode="multiple" to allow selecting more than one option. v-model is then an array, and the selected values are listed below the input (value-placeholder while empty; customize with #value).

No animals selected
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]</script><template>  <ComboBox class="w-[256px]" selection-mode="multiple" label="Favorite Animals" placeholder="Search animals..." value-placeholder="No animals selected" :items="animals" /></template>

In Surface

When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, ComboBox, Form, Surface } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'aardvark', label: 'Aardvark' },  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'kangaroo', label: 'Kangaroo' },  { value: 'panda', label: 'Panda' },  { value: 'snake', label: 'Snake' },]function onSubmit(event: SubmitEvent) {  const data = Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement))  console.log('Form submitted:', data)  alert('Form submitted successfully!')}</script><template>  <Surface class="w-[320px] rounded-3xl p-6">    <Form class="flex w-full flex-col gap-4" @submit.prevent="onSubmit">      <ComboBox required class="w-full" name="animal" variant="secondary" label="Favorite Animal" placeholder="Search animals..." :items="animals" />      <Button type="submit">        Submit      </Button>    </Form>  </Surface></template>

Customization

Tailwind CSS

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { ComboBox } from '@hareui/vue'const itemClass = 'rounded-lg data-[focused=true]:bg-muted/70 data-[selected=true]:font-medium data-[selected=true]:text-foreground'const frameworks: ListBoxItem[] = [  { value: 'react', label: 'React', class: itemClass },  { value: 'vue', label: 'Vue', class: itemClass },  { value: 'svelte', label: 'Svelte', class: itemClass },]</script><template>  <ComboBox    class="w-64 gap-1.5"    label="Framework"    placeholder="Search..."    :items="frameworks"    :ui="{      label: 'font-medium text-neutral-800 dark:text-neutral-100',      inputGroup: 'rounded-xl border border-border/80 bg-surface shadow-sm ring-1 ring-black/5 transition-[box-shadow,border-color] focus-within:ring-2 focus-within:ring-neutral-400/25 dark:ring-white/10 dark:focus-within:ring-neutral-500/30',      trigger: 'text-muted',      popover: 'rounded-xl border border-border/80 bg-surface p-1 shadow-lg ring-1 ring-black/5 dark:ring-white/10',    }"  /></template>

Global Configuration

app.use(createHareUI({
  ui: { comboBox: { slots: { inputGroup: 'rounded-lg', trigger: 'text-muted', popover: 'rounded-lg border border-border bg-surface p-2' } } },
}))

Styling Reference

Interactive States

  • Hover: :hover or [data-hovered="true"] on the trigger
  • Focus: :focus on the input, [data-focus-visible="true"] on the input group
  • Disabled: :disabled or [data-disabled="true"] on the combo box and the trigger
  • Invalid: [data-invalid="true"] on the combo box
  • Open: [data-open="true"] on the combo box and the trigger

API Reference

Props

PropTypeDefaultDescription
itemsListBoxEntry[]-Options, sections ({ type: 'section', label, items }) and separators ({ type: 'separator' }), as in ListBox.
selectionMode"multiple" | "single""single"Whether one or several options can be selected. In 'multiple' mode v-model is an array.
defaultValueComboBoxValueundefinedThe initial selection when uncontrolled (no v-model).
defaultInputValuestring-The initial input text when uncontrolled (no v-model:input-value). Defaults to the selected option's text.
placeholderstring-Temporary text shown in the empty input.
valuePlaceholderstring-Text of the selected-value element (rendered in multiple mode) while nothing is selected.
labelstring-Label text rendered above the input.
descriptionstring-Helper text rendered below the input. Hidden while invalid.
errorMessagestring-Error text rendered below the input while invalid. Defaults to the validation messages.
idstring-Id of the input.
namestring-The name used when submitting an HTML form.
formValue"key" | "text""key"Whether the selected option's value or its text is submitted with forms. With allowsCustomValue the text is always submitted.
openbooleanundefinedWhether the popover is open (v-model:open).
defaultOpenbooleanfalseWhether the popover starts open when uncontrolled.
menuTrigger"input" | "focus" | "manual""focus"The interaction that opens the popover: focusing the input, typing in it, or only the trigger button and arrow keys.
filterfalse | ((text: string, inputValue: string) => boolean)undefinedFilters options against the input text. Defaults to a case- and accent-insensitive "contains" match on each option's textValue ?? label. Pass false to show every item (e.g. when the server already filtered items).
allowsCustomValuebooleanfalseWhether text that matches no option is kept as the value (the selection is cleared) instead of being reverted.
allowsEmptyCollectionbooleanfalseWhether the popover stays open when no option matches (shows the #empty slot).
loadingbooleanundefinedAsync options: shows the #loading slot after the last option and emits load-more at the end of the list (see ListBox loading).
variant"primary" | "secondary"'primary'Visual variant. Use secondary on surfaces.
fullWidthbooleanfalseWhether the combo box takes the full width of its container.
disabledbooleanfalseWhether the combo box is disabled.
readonlybooleanfalseWhether the input can be focused but not changed.
requiredbooleanfalseWhether a value is required before the form can be submitted.
invalidbooleanundefinedControlled invalid state; overrides validation.
validateValidateFn<ComboBoxValidationValue>-Custom validation. Return an error message (or several) when invalid, true / null when valid.
validationBehaviorValidationBehavior'native' (or the enclosing Form's)native blocks form submission and shows errors on commit; aria shows errors in realtime without blocking.
uiComponentSlots<{ slots: { base: string[]; label: string; inputGroup: string; input: string; value: string[]; trigger: string[]; popover: string[]; }; variants: { fullWidth: { false: {}; true: { base: string; inputGroup: string; }; }; }; defaultVariants: { fullWidth: boolean; }; }>-Per-slot class overrides.
modelValueComboBoxValue--
inputValuestring--

Slots

SlotPropsDescription
labelanyLabel content. Replaces label.
valueComboBoxValueSlotPropsContent of the selected-value element below the input (rendered in multiple mode, or whenever this slot is used).
indicator{ open: boolean; }Trigger button content. Defaults to a chevron.
descriptionanyDescription content. Replaces description.
errorValidationResultError content, rendered while invalid.
itemListBoxItemSlotPropsWhole content of each option (as ListBox #item).
item-leadingListBoxItemSlotPropsContent before each option's text (ListBox #item-leading).
item-labelListBoxItemSlotPropsEach option's label (ListBox #item-label).
item-descriptionListBoxItemSlotPropsEach option's description (ListBox #item-description).
item-trailingListBoxItemSlotPropsContent after each option's text (ListBox #item-trailing).
item-indicatorListBoxItemSlotPropsOption checkmark content (ListBox #indicator).
section-label{ section: ListBoxSection; }Section heading (ListBox #section-label).
emptyanyRendered in the popover when no option matches and allowsEmptyCollection is set.
loadinganyShown after the last option while loading is true.

Emits

EventPayloadDescription
update:open[value: boolean]-
update:inputValue[value: string]-
loadMore[]-
update:modelValue[value: ComboBoxValue | undefined]-
update:inputValue[value: string | undefined]-

Accessibility

ComboBox follows the ARIA combobox pattern:

  • Focus stays in the input; the arrow keys, Home / End and Page Up / Page Down move a virtual focus through the options (aria-activedescendant), Enter selects, Escape reverts the text and closes
  • Typing filters the options; on blur, text that matches no option reverts to the selected option (unless allows-custom-value)
  • The trigger button is not in the tab order and keeps focus in the input
  • The label, description and error are linked to the input with aria-labelledby / aria-describedby
  • With name, the selected value (or text) is submitted with forms; required uses native validation

On this page

No Headings