Combobox
A single input field that combines the functionality of a select and input.
Anatomy
To set up the combobox correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Examples
Learn how to use the Combobox component in your project. Let's take a look at the most basic example
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
export const Basic = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{collection.items.map((item) => (
<Combobox.Item key={item} item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
const initialItems = ['React', 'Solid', 'Vue', 'Svelte']
export const Basic = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in collection.items" :key="item" :item="item">
<Combobox.ItemText>{{ item }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/svelte/combobox'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root {collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{#each collection().items as item (item)}
<Combobox.Item {item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Grouping
To group related combobox items, use the groupBy prop on the collection and collection.group() to iterate the
groups.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
export const Grouping = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: contains,
groupBy: (item) => item.type,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{collection.group().map(([type, group]) => (
<Combobox.ItemGroup key={type}>
<Combobox.ItemGroupLabel>{type}</Combobox.ItemGroupLabel>
{group.map((item) => (
<Combobox.Item key={item.value} item={item}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const initialItems = [
{ label: 'React', value: 'react', type: 'JS' },
{ label: 'Solid', value: 'solid', type: 'JS' },
{ label: 'Vue', value: 'vue', type: 'JS' },
{ label: 'Svelte', value: 'svelte', type: 'JS' },
{ label: 'Panda', value: 'panda', type: 'CSS' },
{ label: 'Tailwind', value: 'tailwind', type: 'CSS' },
]
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
export const Grouping = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
groupBy: (item) => item.type,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<For each={collection().group()}>
{([type, group]) => (
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>{type}</Combobox.ItemGroupLabel>
<For each={group}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
)}
</For>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const initialItems = [
{ label: 'React', value: 'react', type: 'JS' },
{ label: 'Solid', value: 'solid', type: 'JS' },
{ label: 'Vue', value: 'vue', type: 'JS' },
{ label: 'Panda', value: 'panda', type: 'CSS' },
{ label: 'Tailwind', value: 'tailwind', type: 'CSS' },
]
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
const initialItems = [
{ label: 'React', value: 'react', type: 'JS' },
{ label: 'Solid', value: 'solid', type: 'JS' },
{ label: 'Vue', value: 'vue', type: 'JS' },
{ label: 'Panda', value: 'panda', type: 'CSS' },
{ label: 'Tailwind', value: 'tailwind', type: 'CSS' },
]
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
groupBy: (item) => item.type,
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup :key="type" v-for="[type, group] in collection.group()">
<Combobox.ItemGroupLabel>{{ type }}</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in group" :key="item.value" :item="item">
<Combobox.ItemText>{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox } from '@ark-ui/svelte/combobox'
import { useListCollection } from '@ark-ui/svelte/collection'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'React', value: 'react', type: 'JS' },
{ label: 'Solid', value: 'solid', type: 'JS' },
{ label: 'Vue', value: 'vue', type: 'JS' },
{ label: 'Svelte', value: 'svelte', type: 'JS' },
{ label: 'Panda', value: 'panda', type: 'CSS' },
{ label: 'Tailwind', value: 'tailwind', type: 'CSS' },
]
const { collection, filter } = useListCollection({
initialItems,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
groupBy: (item) => item.type,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<div>
<Combobox.Root {collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Select framework..." />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{#each collection().group() as [type, group]}
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>{type}</Combobox.ItemGroupLabel>
{#each group as item}
<Combobox.Item {item}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.ItemGroup>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
</div>
Field
The Field component helps manage form-related state and accessibility attributes of a combobox. It includes handling
ARIA labels, helper text, and error text to ensure proper accessibility.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { Field } from '@ark-ui/react/field'
import { useFilter } from '@ark-ui/react/locale'
export const WithField = (props: Field.RootProps) => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Field.Root {...props}>
<Combobox.Root collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Label</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
{collection.items.map((item) => (
<Combobox.Item key={item} item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText>Additional Info</Field.HelperText>
<Field.ErrorText>Error Info</Field.ErrorText>
</Field.Root>
)
}
const initialItems = ['React', 'Solid', 'Vue', 'Svelte']
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { Field } from '@ark-ui/solid/field'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
const initialItems = ['React', 'Solid', 'Vue', 'Svelte']
export const WithField = (props: Field.RootProps) => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Field.Root {...props}>
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label>Label</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText>Additional Info</Field.HelperText>
<Field.ErrorText>Error Info</Field.ErrorText>
</Field.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { Field, type FieldRootProps } from '@ark-ui/vue/field'
import { useFilter } from '@ark-ui/vue/locale'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
const props = defineProps<FieldRootProps>()
</script>
<template>
<Field.Root v-bind="props">
<Combobox.Root :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label>Label</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in collection.items" :key="item" :item="item">
<Combobox.ItemText>{{ item }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText>Additional Info</Field.HelperText>
<Field.ErrorText>Error Info</Field.ErrorText>
</Field.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/svelte/combobox'
import { Field } from '@ark-ui/svelte/field'
import { useFilter } from '@ark-ui/svelte/locale'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Field.Root>
<Combobox.Root {collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
{#each collection().items as item (item)}
<Combobox.Item {item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText>Additional Info</Field.HelperText>
<Field.ErrorText>Error Info</Field.ErrorText>
</Field.Root>
Root Provider
Use the useCombobox hook to create the combobox store and pass it to the Combobox.RootProvider component. This
allows you to have maximum control over the combobox programmatically.
import { Combobox, useCombobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
const initialItems = ['React', 'Solid', 'Vue', 'Svelte']
export const RootProvider = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: contains,
})
const combobox = useCombobox({
collection,
onInputValueChange(details) {
filter(details.inputValue)
},
})
return (
<>
<button onClick={() => combobox.focus()}>Focus</button>
<Combobox.RootProvider value={combobox}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{collection.items.map((item) => (
<Combobox.Item key={item} item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
</>
)
}
import { Combobox, useCombobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
export const RootProvider = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: filterFn().contains,
})
const combobox = useCombobox({
get collection() {
return collection()
},
onInputValueChange(details) {
filter(details.inputValue)
},
})
return (
<>
<button onClick={() => combobox().focus()}>Focus</button>
<Combobox.RootProvider value={combobox}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
</>
)
}
<script setup lang="ts">
import { Combobox, useCombobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: filters.value.contains,
})
const combobox = useCombobox({
get collection() {
return collection.value
},
onInputValueChange(details) {
filter(details.inputValue)
},
})
</script>
<template>
<button @click="combobox.focus()">Focus</button>
<Combobox.RootProvider :value="combobox">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in collection.items" :key="item" :item="item">
<Combobox.ItemText>{{ item }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.RootProvider>
</template>
<script lang="ts">
import { Combobox, useCombobox } from '@ark-ui/svelte/combobox'
import { Portal } from '@ark-ui/svelte/portal'
import { createListCollection } from '@ark-ui/svelte/collection'
const collection = createListCollection({
items: ['React', 'Solid', 'Svelte', 'Vue'],
})
const id = $props.id()
const combobox = useCombobox({ collection, id })
</script>
<Combobox.RootProvider value={combobox}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Select a Framework" />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.List>
{#each collection.items as item}
<Combobox.Item {item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.List>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
If you're using the
Combobox.RootProvidercomponent, you don't need to use theCombobox.Rootcomponent.
Links
Use the asChild prop to render the combobox items as links.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
export const Links = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection} onInputValueChange={handleInputChange} selectionBehavior="preserve">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{collection.items.map((item) => (
<Combobox.Item key={item.value} item={item} asChild>
<a href={item.href}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</a>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const initialItems = [
{ label: 'React', href: 'https://react.dev', value: 'react' },
{ label: 'Solid', href: 'https://solidjs.com', value: 'solid' },
{ label: 'Vue', href: 'https://vuejs.org', value: 'vue' },
{ label: 'Svelte', href: 'https://svelte.dev', value: 'svelte' },
{ label: 'Angular', href: 'https://angular.io', value: 'angular' },
{ label: 'Ember', href: 'https://emberjs.com', value: 'ember' },
{ label: 'Backbone', href: 'https://backbonejs.org', value: 'backbone' },
{ label: 'Polymer', href: 'https://polymer-project.org', value: 'polymer' },
{ label: 'Preact', href: 'https://preactjs.com', value: 'preact' },
{ label: 'Alpine', href: 'https://alpinejs.dev', value: 'alpine' },
{ label: 'Lit', href: 'https://lit.dev', value: 'lit' },
{ label: 'Qwik', href: 'https://qwik.builder.io', value: 'qwik' },
{ label: 'Astro', href: 'https://astro.build', value: 'astro' },
]
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
export const Links = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange} selectionBehavior="preserve">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item} asChild={(props) => <a href={item.href} {...props} />}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const initialItems = [
{ label: 'React', href: 'https://react.dev', value: 'react' },
{ label: 'Solid', href: 'https://solidjs.com', value: 'solid' },
{ label: 'Vue', href: 'https://vuejs.org', value: 'vue' },
{ label: 'Svelte', href: 'https://svelte.dev', value: 'svelte' },
{ label: 'Angular', href: 'https://angular.io', value: 'angular' },
{ label: 'Ember', href: 'https://emberjs.com', value: 'ember' },
{ label: 'Backbone', href: 'https://backbonejs.org', value: 'backbone' },
{ label: 'Polymer', href: 'https://polymer-project.org', value: 'polymer' },
{ label: 'Preact', href: 'https://preactjs.com', value: 'preact' },
{ label: 'Alpine', href: 'https://alpinejs.dev', value: 'alpine' },
{ label: 'Lit', href: 'https://lit.dev', value: 'lit' },
{ label: 'Qwik', href: 'https://qwik.builder.io', value: 'qwik' },
{ label: 'Astro', href: 'https://astro.build', value: 'astro' },
]
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
const initialItems = [
{ label: 'React', href: 'https://react.dev', value: 'react' },
{ label: 'Solid', href: 'https://solidjs.com', value: 'solid' },
{ label: 'Vue', href: 'https://vuejs.org', value: 'vue' },
{ label: 'Svelte', href: 'https://svelte.dev', value: 'svelte' },
{ label: 'Angular', href: 'https://angular.io', value: 'angular' },
{ label: 'Ember', href: 'https://emberjs.com', value: 'ember' },
{ label: 'Backbone', href: 'https://backbonejs.org', value: 'backbone' },
{ label: 'Polymer', href: 'https://polymer-project.org', value: 'polymer' },
{ label: 'Preact', href: 'https://preactjs.com', value: 'preact' },
{ label: 'Alpine', href: 'https://alpinejs.dev', value: 'alpine' },
{ label: 'Lit', href: 'https://lit.dev', value: 'lit' },
{ label: 'Qwik', href: 'https://qwik.builder.io', value: 'qwik' },
{ label: 'Astro', href: 'https://astro.build', value: 'astro' },
]
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :collection="collection" @input-value-change="handleInputChange" selection-behavior="preserve">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :as-child="true">
<a :href="item.href">
<Combobox.ItemText>{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</a>
</Combobox.Item>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox } from '@ark-ui/svelte/combobox'
import { useListCollection } from '@ark-ui/svelte/collection'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'React', href: 'https://react.dev', value: 'react' },
{ label: 'Solid', href: 'https://solidjs.com', value: 'solid' },
{ label: 'Vue', href: 'https://vuejs.org', value: 'vue' },
{ label: 'Svelte', href: 'https://svelte.dev', value: 'svelte' },
{ label: 'Angular', href: 'https://angular.io', value: 'angular' },
{ label: 'Ember', href: 'https://emberjs.com', value: 'ember' },
{ label: 'Backbone', href: 'https://backbonejs.org', value: 'backbone' },
{ label: 'Polymer', href: 'https://polymer-project.org', value: 'polymer' },
{ label: 'Preact', href: 'https://preactjs.com', value: 'preact' },
{ label: 'Alpine', href: 'https://alpinejs.dev', value: 'alpine' },
{ label: 'Lit', href: 'https://lit.dev', value: 'lit' },
{ label: 'Qwik', href: 'https://qwik.builder.io', value: 'qwik' },
{ label: 'Astro', href: 'https://astro.build', value: 'astro' },
]
const { collection, filter } = useListCollection({
initialItems,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<div>
<Combobox.Root {collection} onInputValueChange={handleInputChange} selectionBehavior="preserve">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{#each collection().items as item (item.value)}
<Combobox.Item {item}>
{#snippet asChild(props)}
<a {...props()} href={item.href}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</a>
{/snippet}
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
</div>
For custom router links, you can customize the navigate prop on the Combobox.Root component.
Here's an example of using the Tanstack Router.
import { Combobox } from '@ark-ui/react/combobox'
import { useNavigate } from '@tanstack/react-router'
function Demo() {
const navigate = useNavigate()
return (
<Combobox.Root
navigate={(e) => {
navigate({ to: e.node.href })
}}
>
{/* ... */}
</Combobox.Root>
)
}
Rehydrate Value
In some cases, where a combobox has a defaultValue or value but the collection is not loaded yet, here's how to
rehydrate the value and populate the input value.
import { Combobox, useCombobox, useComboboxContext, useListCollection } from '@ark-ui/react/combobox'
import { Portal } from '@ark-ui/react/portal'
import { useRef, useState } from 'react'
import { useAsync } from 'react-use'
// The meat of the example is here.
// It rehydrates the input value when the combobox is mounted.
function ComboboxRehydrateValue() {
const combobox = useComboboxContext()
const hydrated = useRef(false)
if (combobox.value.length && combobox.collection.size && !hydrated.current) {
combobox.syncSelectedItems()
hydrated.current = true
}
return null
}
export const RehydrateValue = () => {
const [inputValue, setInputValue] = useState('')
const { collection, set } = useListCollection<Character>({
initialItems: [],
itemToString: (item) => item.name,
itemToValue: (item) => item.name,
})
const combobox = useCombobox({
collection,
defaultValue: ['C-3PO'],
placeholder: 'Example: Dexter',
inputValue,
onInputValueChange: (e) => setInputValue(e.inputValue),
})
const state = useAsync(async () => {
const response = await fetch(`https://swapi.py4e.com/api/people/?search=${inputValue}`)
const data = await response.json()
set(data.results)
}, [inputValue, set])
return (
<Combobox.RootProvider value={combobox}>
<Combobox.Label>Search Star Wars Characters</Combobox.Label>
<ComboboxRehydrateValue />
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{state.loading ? (
<span>Loading...</span>
) : state.error ? (
<span>{state.error.message}</span>
) : (
collection.items.map((item) => (
<Combobox.Item key={item.name} item={item}>
<span>
{item.name} - {item.height}cm / {item.mass}kg
</span>
<Combobox.ItemIndicator />
</Combobox.Item>
))
)}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
)
}
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
import { Combobox, useCombobox, useComboboxContext, useListCollection } from '@ark-ui/solid/combobox'
import { For, createEffect, createRenderEffect, createSignal, on } from 'solid-js'
import { Portal } from 'solid-js/web'
import { useAsync } from './use-async'
// The meat of the example is here.
// It rehydrates the input value when the combobox is mounted.
function ComboboxRehydrateValue() {
const combobox = useComboboxContext()
let hydrated = false
createRenderEffect(() => {
if (combobox().value.length && combobox().collection.size && !hydrated) {
combobox().syncSelectedItems()
hydrated = true
}
})
return null
}
export const RehydrateValue = () => {
const [inputValue, setInputValue] = createSignal('')
const { collection, set } = useListCollection<Character>({
initialItems: [],
itemToString: (item) => item.name,
itemToValue: (item) => item.name,
})
const combobox = useCombobox(() => ({
collection: collection(),
defaultValue: ['C-3PO'],
placeholder: 'Example: Dexter',
inputValue: inputValue(),
onInputValueChange: (e) => setInputValue(e.inputValue),
}))
const state = useAsync(async (signal) => {
const response = await fetch(`https://swapi.py4e.com/api/people/?search=${inputValue()}`, { signal })
const data = await response.json()
set(data.results)
})
createEffect(on(inputValue, () => state.load()))
return (
<Combobox.RootProvider value={combobox}>
<Combobox.Label>Search Star Wars Characters</Combobox.Label>
<ComboboxRehydrateValue />
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{state.loading() ? (
<span>Loading...</span>
) : state.error() ? (
<span>{state.error()?.message}</span>
) : (
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<span>
{item.name} - {item.height}cm / {item.mass}kg
</span>
<Combobox.ItemIndicator />
</Combobox.Item>
)}
</For>
)}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
)
}
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, type UseComboboxProps, useCombobox, useListCollection } from '@ark-ui/vue/combobox'
import { computed, ref, watch, watchEffect } from 'vue'
import { useAsync } from './use-async'
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
const inputValue = ref('')
const { collection, set } = useListCollection<Character>({
initialItems: [],
itemToString: (item) => item.name,
itemToValue: (item) => item.name,
})
const combobox = useCombobox(
computed<UseComboboxProps<Character>>(() => ({
collection: collection.value,
defaultValue: ['C-3PO'],
placeholder: 'Example: Dexter',
inputValue: inputValue.value,
onInputValueChange: (e) => {
inputValue.value = e.inputValue
},
})),
)
const fetchData = computed(() => async (signal: AbortSignal | null) => {
const response = await fetch(`https://swapi.py4e.com/api/people/?search=${inputValue.value}`, { signal })
const data = await response.json()
set(data.results)
})
const state = useAsync(fetchData)
watch(inputValue, () => {
state.load()
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
inputValue.value = details.inputValue
}
// The meat of the example is here.
// It rehydrates the input value when the combobox is mounted.
let hydrated = false
watchEffect(() => {
if (combobox.value.value.length && combobox.value.collection.size && !hydrated) {
combobox.value.syncSelectedItems()
hydrated = true
}
})
</script>
<template>
<Combobox.Root
:collection="collection"
:default-value="['C-3PO']"
placeholder="Example: Dexter"
:input-value="inputValue"
@input-value-change="handleInputChange"
>
<Combobox.Label>Search Star Wars Characters</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<span v-if="state.loading.value">Loading...</span>
<span v-else-if="state.error.value">{{ state.error.value.message }}</span>
<template v-else>
<Combobox.Item v-for="item in collection.items" :key="item.name" :item="item">
<span>{{ item.name }} - {{ item.height }}cm / {{ item.mass }}kg</span>
<Combobox.ItemIndicator />
</Combobox.Item>
</template>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
import { Combobox, useCombobox, useListCollection } from '@ark-ui/svelte/combobox'
import { Portal } from '@ark-ui/svelte/portal'
import { useAsync } from './use-async.svelte'
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
const { collection, set } = useListCollection<Character>({
initialItems: [],
itemToString: (item) => item.name,
itemToValue: (item) => item.name,
})
let inputValue = $state('')
const combobox = useCombobox(() => ({
collection: collection(),
defaultValue: ['C-3PO'],
placeholder: 'Example: Dexter',
inputValue,
onInputValueChange: (e) => {
inputValue = e.inputValue
},
}))
const fetchData = $derived(async (signal: AbortSignal | null) => {
const response = await fetch(`https://swapi.py4e.com/api/people/?search=${inputValue}`, { signal })
const data = await response.json()
set(data.results)
})
const _state = useAsync(() => fetchData)
$effect(() => {
void inputValue
void _state.load()
})
// The meat of the example is here.
// It rehydrates the input value when the combobox is mounted.
let hydrated = false
$effect.pre(() => {
if (combobox().value.length && combobox().collection.size && !hydrated) {
combobox().syncSelectedItems()
hydrated = true
}
})
</script>
<Combobox.RootProvider value={combobox}>
<Combobox.Label>Search Star Wars Characters</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{#if _state.loading()}
<span>Loading...</span>
{:else if _state.error()}
<span>{_state.error()?.message}</span>
{:else}
{#each collection().items as item (item.name)}
<Combobox.Item {item}>
<span>
{item.name} - {item.height}cm / {item.mass}kg
</span>
<Combobox.ItemIndicator />
</Combobox.Item>
{/each}
{/if}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
Highlight Matching Text
Here's an example of highlighting the search text in combobox items based on the user's input.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { Highlight } from '@ark-ui/react/highlight'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
export const WithHighlight = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{collection.items.map((item) => (
<Combobox.Item key={item} item={item}>
<Combobox.ItemText>
<Combobox.Context>
{(context) => <Highlight text={item} query={context.inputValue} ignoreCase />}
</Combobox.Context>
</Combobox.ItemText>
</Combobox.Item>
))}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { Highlight } from '@ark-ui/solid/highlight'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
const initialItems = ['React', 'Solid', 'Vue', 'Svelte']
export const WithHighlight = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>
<Combobox.Context>
{(context) => <Highlight text={item} query={context().inputValue} ignoreCase />}
</Combobox.Context>
</Combobox.ItemText>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { Highlight } from '@ark-ui/vue/highlight'
import { useFilter } from '@ark-ui/vue/locale'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in collection.items" :key="item" :item="item">
<Combobox.ItemText>
<Combobox.Context v-slot="context">
<Highlight :text="item" :query="context.inputValue" ignore-case>
{{ item }}
</Highlight>
</Combobox.Context>
</Combobox.ItemText>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/svelte/combobox'
import { Highlight } from '@ark-ui/svelte/highlight'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: ['React', 'Solid', 'Vue', 'Svelte'],
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root {collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{#each collection().items as item (item)}
<Combobox.Item {item}>
<Combobox.ItemText>
<Combobox.Context>
{#snippet render(context)}
<Highlight text={item} query={context().inputValue} ignoreCase />
{/snippet}
</Combobox.Context>
</Combobox.ItemText>
</Combobox.Item>
{/each}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Dynamic Items
Generate combobox items dynamically based on user input. This is useful for creating suggestions or autocomplete functionality.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { Portal } from '@ark-ui/react/portal'
const suggestList = ['gmail.com', 'yahoo.com', 'ark-ui.com']
export const Dynamic = () => {
const { collection, set } = useListCollection<string>({
initialItems: [],
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
if (details.reason === 'input-change') {
const items = suggestList.map((item) => `${details.inputValue}@${item}`)
set(items)
}
}
return (
<Combobox.Root collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{collection.items.map((item) => (
<Combobox.Item key={item} item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
const suggestList = ['gmail.com', 'yahoo.com', 'ark-ui.com']
export const Dynamic = () => {
const { collection, set } = useListCollection<string>({
initialItems: [],
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
if (details.reason === 'input-change') {
const items = suggestList.map((item) => `${details.inputValue}@${item}`)
set(items)
}
}
return (
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
const suggestList = ['gmail.com', 'yahoo.com', 'ark-ui.com']
const { collection, set } = useListCollection<string>({
initialItems: [],
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
if (details.reason === 'input-change') {
const items = suggestList.map((item) => `${details.inputValue}@${item}`)
set(items)
}
}
</script>
<template>
<Combobox.Root :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Item v-for="item in collection.items" :key="item" :item="item">
<Combobox.ItemText>{{ item }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/svelte/combobox'
import { Portal } from '@ark-ui/svelte/portal'
const suggestList = ['gmail.com', 'yahoo.com', 'ark-ui.com']
const { collection, set } = useListCollection<string>({
initialItems: [],
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
if (details.reason === 'input-change') {
const items = suggestList.map((item) => `${details.inputValue}@${item}`)
set(items)
}
}
</script>
<Combobox.Root {collection} onInputValueChange={handleInputChange}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{#each collection().items as item (item)}
<Combobox.Item {item}>
<Combobox.ItemText>{item}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Creatable Options
Allow users to create new options when their search doesn't match any existing items. This is useful for tags, categories, or other custom values.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
import { useState } from 'react'
import { flushSync } from 'react-dom'
interface Item {
label: string
value: string
__new__?: boolean
}
const NEW_OPTION_VALUE = '[[new]]'
const createNewOption = (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE })
const isNewOptionValue = (value: string) => value === NEW_OPTION_VALUE
const replaceNewOptionValue = (values: string[], value: string) =>
values.map((v) => (v === NEW_OPTION_VALUE ? value : v))
const getNewOptionData = (inputValue: string): Item => ({ label: inputValue, value: inputValue, __new__: true })
export const Creatable = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter, upsert, update, remove } = useListCollection<Item>({
initialItems: [
{ label: 'React', value: 'react' },
{ label: 'Solid', value: 'solid' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
],
filter: contains,
})
const isValidNewOption = (inputValue: string) => {
const exactOptionMatch = collection.filter((item) => item.toLowerCase() === inputValue.toLowerCase()).size > 0
return !exactOptionMatch && inputValue.trim().length > 0
}
const [selectedValue, setSelectedValue] = useState<string[]>([])
const [inputValue, setInputValue] = useState('')
const handleInputChange = ({ inputValue, reason }: Combobox.InputValueChangeDetails) => {
if (reason === 'input-change' || reason === 'item-select') {
flushSync(() => {
if (isValidNewOption(inputValue)) {
upsert(NEW_OPTION_VALUE, createNewOption(inputValue))
} else if (inputValue.trim().length === 0) {
remove(NEW_OPTION_VALUE)
}
})
filter(inputValue)
}
setInputValue(inputValue)
}
const handleOpenChange = ({ reason }: Combobox.OpenChangeDetails) => {
if (reason === 'trigger-click') {
filter('')
}
}
const handleValueChange = ({ value }: Combobox.ValueChangeDetails) => {
setSelectedValue(replaceNewOptionValue(value, inputValue))
if (value.includes(NEW_OPTION_VALUE)) {
console.log('New Option Created', inputValue)
update(NEW_OPTION_VALUE, getNewOptionData(inputValue))
}
}
return (
<Combobox.Root
collection={collection}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
value={selectedValue}
onValueChange={handleValueChange}
allowCustomValue
>
<Combobox.Control>
<Combobox.Input placeholder="Search..." />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{collection.items.map((item) => (
<Combobox.Item key={item.value} item={item}>
{isNewOptionValue(item.value) ? (
<Combobox.ItemText>+ Create "{item.label}"</Combobox.ItemText>
) : (
<Combobox.ItemText>
{item.label} {item.__new__ ? NEW_OPTION_VALUE : ''}
</Combobox.ItemText>
)}
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { createSignal, For } from 'solid-js'
import { Portal } from 'solid-js/web'
interface Item {
label: string
value: string
__new__?: boolean
}
const NEW_OPTION_VALUE = '[[new]]'
const createNewOption = (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE })
const isNewOptionValue = (value: string) => value === NEW_OPTION_VALUE
const replaceNewOptionValue = (values: string[], value: string) =>
values.map((v) => (v === NEW_OPTION_VALUE ? value : v))
const getNewOptionData = (inputValue: string): Item => ({ label: inputValue, value: inputValue, __new__: true })
export const Creatable = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter, upsert, update, remove } = useListCollection<Item>({
initialItems: [
{ label: 'React', value: 'react' },
{ label: 'Solid', value: 'solid' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
],
filter: filterFn().contains,
})
const isValidNewOption = (inputValue: string) => {
const exactOptionMatch =
collection().items.filter((item) => item.label.toLowerCase() === inputValue.toLowerCase()).length > 0
return !exactOptionMatch && inputValue.trim().length > 0
}
const [selectedValue, setSelectedValue] = createSignal<string[]>([])
const [inputValue, setInputValue] = createSignal('')
const handleInputChange = ({ inputValue: newInputValue, reason }: Combobox.InputValueChangeDetails) => {
if (reason === 'input-change' || reason === 'item-select') {
if (isValidNewOption(newInputValue)) {
upsert(NEW_OPTION_VALUE, createNewOption(newInputValue))
} else if (newInputValue.trim().length === 0) {
remove(NEW_OPTION_VALUE)
}
filter(newInputValue)
}
setInputValue(newInputValue)
}
const handleOpenChange = ({ reason }: Combobox.OpenChangeDetails) => {
if (reason === 'trigger-click') {
filter('')
}
}
const handleValueChange = ({ value }: Combobox.ValueChangeDetails) => {
setSelectedValue(replaceNewOptionValue(value, inputValue()))
if (value.includes(NEW_OPTION_VALUE)) {
console.log('New Option Created', inputValue())
update(NEW_OPTION_VALUE, getNewOptionData(inputValue()))
}
}
return (
<Combobox.Root
collection={collection()}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
value={selectedValue()}
onValueChange={handleValueChange}
allowCustomValue
>
<Combobox.Control>
<Combobox.Input placeholder="Search..." />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
{isNewOptionValue(item.value) ? (
<Combobox.ItemText>+ Create "{item.label}"</Combobox.ItemText>
) : (
<Combobox.ItemText>
{item.label} {item.__new__ ? NEW_OPTION_VALUE : ''}
</Combobox.ItemText>
)}
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
import { nextTick, ref, Teleport } from 'vue'
interface Item {
label: string
value: string
__new__?: boolean
}
const NEW_OPTION_VALUE = '[[new]]'
const createNewOption = (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE })
const isNewOptionValue = (value: string) => value === NEW_OPTION_VALUE
const replaceNewOptionValue = (values: string[], value: string) =>
values.map((v) => (v === NEW_OPTION_VALUE ? value : v))
const getNewOptionData = (inputValue: string): Item => ({ label: inputValue, value: inputValue, __new__: true })
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter, upsert, update, remove } = useListCollection<Item>({
initialItems: [
{ label: 'React', value: 'react' },
{ label: 'Solid', value: 'solid' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
],
filter: filterFn.value.contains,
})
const isValidNewOption = (inputValue: string) => {
const exactOptionMatch =
collection.value.items.filter((item) => item.label.toLowerCase() === inputValue.toLowerCase()).length > 0
return !exactOptionMatch && inputValue.trim().length > 0
}
const selectedValue = ref<string[]>([])
const inputValue = ref('')
const handleInputChange = ({ inputValue: newInputValue, reason }: Combobox.InputValueChangeDetails) => {
if (reason === 'input-change' || reason === 'item-select') {
if (isValidNewOption(newInputValue)) {
upsert(NEW_OPTION_VALUE, createNewOption(newInputValue))
} else if (newInputValue.trim().length === 0) {
remove(NEW_OPTION_VALUE)
}
filter(newInputValue)
}
inputValue.value = newInputValue
}
const handleOpenChange = ({ reason }: Combobox.OpenChangeDetails) => {
if (reason === 'trigger-click') {
filter('')
}
}
const handleValueChange = async ({ value }: Combobox.ValueChangeDetails) => {
await nextTick()
selectedValue.value = replaceNewOptionValue(value, inputValue.value)
if (value.includes(NEW_OPTION_VALUE)) {
console.log('New Option Created', inputValue.value)
update(NEW_OPTION_VALUE, getNewOptionData(inputValue.value))
}
}
</script>
<template>
<Combobox.Root
:collection="collection"
:model-value="selectedValue"
:onInputValueChange="handleInputChange"
:onOpenChange="handleOpenChange"
:onValueChange="handleValueChange"
allowCustomValue
>
<Combobox.Control>
<Combobox.Input placeholder="Search..." />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item">
<Combobox.ItemText v-if="isNewOptionValue(item.value)">+ Create "{{ item.label }}"</Combobox.ItemText>
<Combobox.ItemText v-else>{{ item.label }} {{ item.__new__ ? NEW_OPTION_VALUE : '' }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
import { Combobox, useListCollection } from '@ark-ui/svelte/combobox'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import { tick } from 'svelte'
interface Item {
label: string
value: string
__new__?: boolean
}
const NEW_OPTION_VALUE = '[[new]]'
const createNewOption = (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE })
const isNewOptionValue = (value: string) => value === NEW_OPTION_VALUE
const replaceNewOptionValue = (values: string[], value: string) =>
values.map((v) => (v === NEW_OPTION_VALUE ? value : v))
const getNewOptionData = (inputValue: string): Item => ({ label: inputValue, value: inputValue, __new__: true })
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter, upsert, update, remove } = useListCollection<Item>({
initialItems: [
{ label: 'React', value: 'react' },
{ label: 'Solid', value: 'solid' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
],
filter: filterFn().contains,
})
const isValidNewOption = (inputValue: string) => {
const exactOptionMatch =
collection().items.filter((item) => item.label.toLowerCase() === inputValue.toLowerCase()).length > 0
return !exactOptionMatch && inputValue.trim().length > 0
}
let selectedValue: string[] = $state([])
let inputValue: string = $state('')
const handleInputChange = ({ inputValue: newInputValue, reason }: Combobox.InputValueChangeDetails) => {
if (reason === 'input-change' || reason === 'item-select') {
if (isValidNewOption(newInputValue)) {
upsert(NEW_OPTION_VALUE, createNewOption(newInputValue))
} else if (newInputValue.trim().length === 0) {
remove(NEW_OPTION_VALUE)
}
filter(newInputValue)
}
inputValue = newInputValue
}
const handleOpenChange = ({ reason }: Combobox.OpenChangeDetails) => {
if (reason === 'trigger-click') {
filter('')
}
}
const handleValueChange = async ({ value }: Combobox.ValueChangeDetails) => {
await tick()
selectedValue = replaceNewOptionValue(value, inputValue)
if (value.includes(NEW_OPTION_VALUE)) {
console.log('New Option Created', inputValue)
update(NEW_OPTION_VALUE, getNewOptionData(inputValue))
}
}
</script>
<Combobox.Root
{collection}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
value={selectedValue}
onValueChange={handleValueChange}
allowCustomValue
>
<Combobox.Control>
<Combobox.Input placeholder="Search..." />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{#each collection().items as item (item.value)}
<Combobox.Item {item}>
{#if isNewOptionValue(item.value)}
<Combobox.ItemText>+ Create "{item.label}"</Combobox.ItemText>
{:else}
<Combobox.ItemText>
{item.label}
{item.__new__ ? NEW_OPTION_VALUE : ''}
</Combobox.ItemText>
{/if}
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Custom Objects
By default, the combobox collection expects an array of objects with label and value properties. In some cases, you
may need to deal with custom objects.
Use the itemToString and itemToValue props to map the custom object to the required interface.
const items = [
{ country: 'United States', code: 'US', flag: '🇺🇸' },
{ country: 'Canada', code: 'CA', flag: '🇨🇦' },
{ country: 'Australia', code: 'AU', flag: '🇦🇺' },
// ...
]
const { collection } = useListCollection({
initialItems: items,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
})
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
export const CustomObject = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ country: 'United States', code: 'US', flag: '🇺🇸' },
{ country: 'Canada', code: 'CA', flag: '🇨🇦' },
{ country: 'Australia', code: 'AU', flag: '🇦🇺' },
],
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{collection.items.map((item) => (
<Combobox.Item key={item.country} item={item}>
<Combobox.ItemText>{item.country}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
export const CustomObject = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ country: 'United States', code: 'US', flag: '🇺🇸' },
{ country: 'Canada', code: 'CA', flag: '🇨🇦' },
{ country: 'Australia', code: 'AU', flag: '🇦🇺' },
],
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Countries</Combobox.ItemGroupLabel>
<For each={collection().items}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item.country}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ country: 'United States', code: 'US', flag: '🇺🇸' },
{ country: 'Canada', code: 'CA', flag: '🇨🇦' },
{ country: 'Australia', code: 'AU', flag: '🇦🇺' },
],
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :collection="collection" @input-value-change="handleInputChange">
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Countries</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in collection.items" :key="item.code" :item="item">
<Combobox.ItemText>{{ item.country }}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.Root>
</template>
<script lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/svelte/combobox'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ country: 'United States', code: 'US', flag: '🇺🇸' },
{ country: 'Canada', code: 'CA', flag: '🇨🇦' },
{ country: 'Australia', code: 'AU', flag: '🇦🇺' },
],
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root {collection} onInputValueChange={handleInputChange}>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>Open</Combobox.Trigger>
<Combobox.ClearTrigger>Clear</Combobox.ClearTrigger>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Countries</Combobox.ItemGroupLabel>
{#each collection().items as item (item.code)}
<Combobox.Item {item}>
<Combobox.ItemText>{item.country}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Guides
Type-Safety
The Combobox.RootComponent type enables you to create closed, strongly typed wrapper components that maintain full
type safety for collection items.
This is particularly useful when building reusable combobox components with custom props and consistent styling.
import { Combobox as ArkCombobox, type CollectionItem } from '@ark-ui/react/combobox'
import { useListCollection } from '@ark-ui/react/collection'
interface ComboboxProps<T extends CollectionItem> extends ArkCombobox.RootProps<T> {}
const Combobox: ArkCombobox.RootComponent = (props) => {
return <ArkCombobox.Root {...props}>{/* ... */}</ArkCombobox.Root>
}
Then, you can use the Combobox component as follows:
const App = () => {
const { collection } = useListCollection({
initialItems: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
],
})
return (
<Combobox
collection={collection}
onValueChange={(e) => {
// this will be strongly typed Array<{ label: string, value: string }>
console.log(e.items)
}}
>
{/* ... */}
</Combobox>
)
}
Limit Large Datasets
The recommended way of managing large lists is to use the limit property on the useListCollection hook. This will
limit the number of rendered items in the DOM to improve performance.
const { collection } = useListCollection({
initialItems: items,
limit: 10,
})
Available height and width
The following css variables are exposed to the Combobox.Positioner which you can use to style the Combobox.Content
/* width of the combobox control */
--reference-width: <pixel-value>;
/* width of the available viewport */
--available-width: <pixel-value>;
/* height of the available viewport */
--available-height: <pixel-value>;
For example, if you want to make sure the maximum height doesn't exceed the available height, you can use the following:
[data-scope='combobox'][data-part='content'] {
max-height: calc(var(--available-height) - 100px);
}
API Reference
Root
| Prop | Default | Type |
|---|---|---|
collection | ListCollection<T>The collection of items | |
allowCustomValue | booleanWhether to allow typing custom values in the input | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
autoFocus | booleanWhether to autofocus the input on mount | |
closeOnSelect | booleanWhether to close the combobox when an item is selected. | |
composite | true | booleanWhether the combobox is a composed with other composite widgets like tabs |
defaultHighlightedValue | stringThe initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. | |
defaultInputValue | '' | stringThe initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. |
defaultOpen | booleanThe initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. | |
defaultValue | [] | string[]The initial value of the combobox's selected items when rendered. Use when you don't need to control the value of the combobox's selected items. |
disabled | booleanWhether the combobox is disabled | |
disableLayer | booleanWhether to disable registering this a dismissable layer | |
form | stringThe associate form of the combobox. | |
highlightedValue | stringThe controlled highlighted value of the combobox | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
label: string
control: string
input: string
content: string
trigger: string
clearTrigger: string
item: (id: string, index?: number | undefined) => string
positioner: string
itemGroup: (id: string | number) => string
itemGroupLabel: (id: string | number) => string
}>The ids of the elements in the combobox. Useful for composition. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
inputBehavior | 'none' | 'none' | 'autohighlight' | 'autocomplete'Defines the auto-completion behavior of the combobox. - `autohighlight`: The first focused item is highlighted as the user types - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated |
inputValue | stringThe controlled value of the combobox's input | |
invalid | booleanWhether the combobox is invalid | |
lazyMount | false | booleanWhether to enable lazy mounting |
loopFocus | true | booleanWhether to loop the keyboard navigation through the items |
multiple | booleanWhether to allow multiple selection. **Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`. It is recommended to render the selected items in a separate container. | |
name | stringThe `name` attribute of the combobox's input. Useful for form submission | |
navigate | (details: NavigateDetails) => voidFunction to navigate to the selected item | |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component | |
onHighlightChange | (details: HighlightChangeDetails<T>) => voidFunction called when an item is highlighted using the pointer or keyboard navigation. | |
onInputValueChange | (details: InputValueChangeDetails) => voidFunction called when the input's value changes | |
onInteractOutside | (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => voidFunction called when the popup is opened | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component | |
onSelect | (details: SelectionDetails) => voidFunction called when an item is selected | |
onValueChange | (details: ValueChangeDetails<T>) => voidFunction called when a new item is selected | |
open | booleanThe controlled open state of the combobox | |
openOnChange | true | boolean | ((details: InputValueChangeDetails) => boolean)Whether to show the combobox when the input value changes |
openOnClick | false | booleanWhether to open the combobox popup on initial click on the input |
openOnKeyPress | true | booleanWhether to open the combobox on arrow key press |
placeholder | stringThe placeholder text of the combobox's input | |
positioning | { placement: 'bottom-start' } | PositioningOptionsThe positioning options to dynamically position the menu |
present | booleanWhether the node is present (controlled by the user) | |
readOnly | booleanWhether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it | |
required | booleanWhether the combobox is required | |
scrollToIndexFn | (details: ScrollToIndexDetails) => voidFunction to scroll to a specific index | |
selectionBehavior | 'replace' | 'replace' | 'clear' | 'preserve'The behavior of the combobox input when an item is selected - `replace`: The selected item string is set as the input value - `clear`: The input value is cleared - `preserve`: The input value is preserved |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
translations | IntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their states | |
unmountOnExit | false | booleanWhether to unmount on exit. |
value | string[]The controlled value of the combobox's selected items |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | root |
[data-invalid] | Present when invalid |
[data-readonly] | Present when read-only |
ClearTrigger
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | clear-trigger |
[data-invalid] | Present when invalid |
Content
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| CSS Variable | Description |
|---|---|
--layer-index | The index of the dismissable in the layer stack |
--nested-layer-count | The number of nested comboboxs |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | content |
[data-state] | "open" | "closed" |
[data-nested] | listbox |
[data-has-nested] | listbox |
[data-placement] | The placement of the content |
[data-empty] | Present when the content is empty |
Control
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | control |
[data-state] | "open" | "closed" |
[data-focus] | Present when focused |
[data-disabled] | Present when disabled |
[data-invalid] | Present when invalid |
Empty
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
Input
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | input |
[data-invalid] | Present when invalid |
[data-autofocus] | |
[data-state] | "open" | "closed" |
ItemGroupLabel
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
ItemGroup
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item-group |
[data-empty] | Present when the content is empty |
ItemIndicator
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item-indicator |
[data-state] | "checked" | "unchecked" |
Item
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
item | anyThe item to render | |
persistFocus | booleanWhether hovering outside should clear the highlighted state |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item |
[data-highlighted] | Present when highlighted |
[data-state] | "checked" | "unchecked" |
[data-disabled] | Present when disabled |
[data-value] | The value of the item |
ItemText
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item-text |
[data-state] | "checked" | "unchecked" |
[data-disabled] | Present when disabled |
[data-highlighted] | Present when highlighted |
Label
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | label |
[data-readonly] | Present when read-only |
[data-disabled] | Present when disabled |
[data-invalid] | Present when invalid |
[data-focus] | Present when focused |
List
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | list |
[data-empty] | Present when the content is empty |
Positioner
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| CSS Variable | Description |
|---|---|
--transform-origin | The transform origin for animations |
--reference-width | The width of the reference element |
--available-width | The available width in viewport |
--available-height | The available height in viewport |
--x | The x position for transform |
--y | The y position for transform |
--z-index | The z-index value |
--reference-height | The height of the root |
RootProvider
| Prop | Default | Type |
|---|---|---|
value | UseComboboxReturn<T> | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
lazyMount | false | booleanWhether to enable lazy mounting |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
present | booleanWhether the node is present (controlled by the user) | |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
unmountOnExit | false | booleanWhether to unmount on exit. |
Trigger
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
focusable | booleanWhether the trigger is focusable |
| Data Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | trigger |
[data-state] | "open" | "closed" |
[data-invalid] | Present when invalid |
[data-focusable] | |
[data-readonly] | Present when read-only |
[data-disabled] | Present when disabled |
Accessibility
Complies with the Combobox WAI-ARIA design pattern.
Keyboard Support
| Key | Description |
|---|---|
ArrowDown | When the combobox is closed, opens the listbox and highlights to the first option. When the combobox is open, moves focus to the next option. |
ArrowUp | When the combobox is closed, opens the listbox and highlights to the last option. When the combobox is open, moves focus to the previous option. |
Home | When the combobox is open, moves focus to the first option. |
End | When the combobox is open, moves focus to the last option. |
Escape | Closes the listbox. |
Enter | Selects the highlighted option and closes the combobox. |
Esc | Closes the combobox |