Combobox
A text input paired with a listbox, narrowed as the user types.
Combobox pairs a text input with a listbox, so the user narrows the list by typing and then either picks a match or keeps what they typed. Useful when typing beats scrolling, or when the options come from a server.
<template>
<Combobox.Root
v-model="value"
v-model:input-value="query"
:options="options"
>
<Combobox.InputGroup
class="group border-surface rounded-component-lg has-[:focus-visible]:focus-ring data-[content-open=true]:focus-ring relative isolate flex h-12 w-64 cursor-text items-center justify-between gap-1.5 border-2 px-3.5 outline-4 outline-transparent transition-all duration-100 ease-linear"
>
<label
class="flex max-h-full w-full cursor-text flex-col-reverse justify-center gap-0 px-1 transition-all duration-100 ease-linear group-has-[:focus]:gap-1 group-has-[input:not(:placeholder-shown)]:gap-1 before:absolute before:-inset-0.5 before:-z-10 before:content-['']"
>
<span
class="relative flex h-0 w-full items-end transition-all duration-100 ease-linear group-has-[:focus]:h-[0.9375rem] group-has-[input:not(:placeholder-shown)]:h-[0.9375rem]"
>
<Combobox.Input
class="text-primary-solid type-component-lg -my-1.25 box-content block h-[1lh] w-full min-w-0 bg-transparent py-1.25 [line-height:normal]! transition-colors duration-100 ease-linear outline-none placeholder:text-transparent"
placeholder=" "
/>
</span>
<span
class="text-primary-muted type-component-lg group-has-[:focus]:type-component-2xs group-has-[input:not(:placeholder-shown)]:type-component-2xs flex w-full items-center [line-height:normal]! transition-all duration-100 ease-linear"
>
City
</span>
</label>
<Combobox.Clear
class="text-primary-muted hover:text-primary-solid flex shrink-0 items-center transition-all duration-100 ease-linear"
>
<svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
<path
d="m4.5 4.5 7 7m0-7-7 7"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</Combobox.Clear>
<Combobox.Trigger
class="text-primary-muted group-has-[input:not(:placeholder-shown)]:text-primary-solid flex shrink-0 items-center transition-all duration-100 ease-linear"
>
<svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
<path
d="m4 6.5 4 4 4-4"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</Combobox.Trigger>
</Combobox.InputGroup>
<Combobox.Content
class="rounded-component-2xl bg-primary-inverted shadow-component-high w-(--rack-floating-anchor-width) overflow-y-auto p-1.5 backdrop-blur-[8rem]"
>
<Combobox.Item
v-for="city in matches"
:key="city"
class="group/item rounded-component-md type-component-lg text-primary-on-subtle hover:bg-primary-subtle active:bg-primary-subtle-hover data-[highlighted=true]:bg-primary-subtle data-[disabled=true]:text-disabled-on-subtle flex h-12 w-full items-center justify-start gap-4 px-1 transition-colors duration-100 ease-linear select-none [--mirror-combobox-item-cursor:pointer]"
:value="city"
>
<Combobox.ItemText
class="inline-flex h-full min-w-0 flex-1 items-center overflow-hidden pr-1.5 pl-5.5"
>
<span class="truncate">{{ city }}</span>
</Combobox.ItemText>
<Combobox.ItemIndicator
class="text-primary-muted group-hover/item:text-primary-solid group-data-[highlighted=true]/item:text-primary-solid flex shrink-0 items-center pr-4 leading-none transition-colors duration-100 ease-linear"
>
<svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
<path
d="m3.5 8.5 3 3 6-6.5"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</Combobox.ItemIndicator>
</Combobox.Item>
<Combobox.Empty
class="type-component-lg text-primary-muted flex h-12 items-center pl-6.5"
>
No matches.
</Combobox.Empty>
</Combobox.Content>
</Combobox.Root>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Combobox } from '@maas/mirror/vue/Combobox'
const options = {
modal: false,
teleport: { disabled: true },
}
const cities = ['Glasgow', 'Bath', 'Nuremberg', 'Munich', 'Rome']
const value = ref<string | null>(null)
const query = ref('')
const matches = computed(() =>
cities.filter((city) =>
city.toLowerCase().includes(query.value.toLowerCase())
)
)
</script>
Usage guidelines
- We do not filter for you. Bind the text with
v-model:input-valueand render your own filtered array, so a search against a server works exactly like a local one.must - Make sure the combobox has an accessible name, either through a
Labelinside aFieldor anaria-labelon the input.must Combobox.Contentteleports to the end of<body>unless you turn that off withoptions.teleport.disabled. Leave it on where the field sits inside anything withoverflow: hidden.should- The content resolves everything but
sideandalignonce, during setup, so change the rest before the popup first renders or key the root to remount it.must