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.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
export const Basic = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' },
{ label: 'Mango', value: 'mango' },
{ label: 'Pineapple', value: 'pineapple' },
{ label: 'Strawberry', value: 'strawberry' },
],
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root className={styles.Root} collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label className={styles.Label}>Favorite Fruit</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Apple" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-solid'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
import styles from 'styles/combobox.module.css'
export const Basic = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' },
{ label: 'Mango', value: 'mango' },
{ label: 'Pineapple', value: 'pineapple' },
{ label: 'Strawberry', value: 'strawberry' },
],
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root class={styles.Root} collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Favorite Fruit</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Apple" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</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'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Date', value: 'date' },
{ label: 'Elderberry', value: 'elderberry' },
{ label: 'Fig', value: 'fig' },
],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :class="styles.Root" :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label :class="styles.Label">Fruit</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Apple" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Empty :class="styles.Item">No results found</Combobox.Empty>
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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 { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Date', value: 'date' },
{ label: 'Elderberry', value: 'elderberry' },
{ label: 'Fig', value: 'fig' },
],
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Fruit</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Apple" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No results found</Combobox.Empty>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Auto Highlight
Automatically highlight the first matching item as the user types by setting inputBehavior="autohighlight".
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
export const AutoHighlight = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Finance', value: 'finance' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Operations', value: 'operations' },
{ label: 'Product', value: 'product' },
{ label: 'Customer Success', value: 'customer-success' },
{ label: 'Legal', value: 'legal' },
{ label: 'Information Technology', value: 'information-technology' },
{ label: 'Design', value: 'design' },
],
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
className={styles.Root}
collection={collection}
onInputValueChange={handleInputChange}
inputBehavior="autohighlight"
>
<Combobox.Label className={styles.Label}>Department</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Engineering" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
<Combobox.Empty className={styles.Item}>No results found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</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'
import styles from 'styles/combobox.module.css'
export const AutoHighlight = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Finance', value: 'finance' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Operations', value: 'operations' },
{ label: 'Product', value: 'product' },
{ label: 'Customer Success', value: 'customer-success' },
{ label: 'Legal', value: 'legal' },
{ label: 'Information Technology', value: 'information-technology' },
{ label: 'Design', value: 'design' },
],
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
class={styles.Root}
collection={collection()}
onInputValueChange={handleInputChange}
inputBehavior="autohighlight"
>
<Combobox.Label class={styles.Label}>Department</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Engineering" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No results found</Combobox.Empty>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.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'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Finance', value: 'finance' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Operations', value: 'operations' },
{ label: 'Product', value: 'product' },
{ label: 'Customer Success', value: 'customer-success' },
{ label: 'Legal', value: 'legal' },
{ label: 'Information Technology', value: 'information-technology' },
{ label: 'Design', value: 'design' },
],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root
:class="styles.Root"
:collection="collection"
@input-value-change="handleInputChange"
input-behavior="autohighlight"
>
<Combobox.Label :class="styles.Label">Department</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Engineering" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Empty :class="styles.Item">No results found</Combobox.Empty>
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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 { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Finance', value: 'finance' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Operations', value: 'operations' },
{ label: 'Product', value: 'product' },
{ label: 'Customer Success', value: 'customer-success' },
{ label: 'Legal', value: 'legal' },
{ label: 'Information Technology', value: 'information-technology' },
{ label: 'Design', value: 'design' },
],
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange} inputBehavior="autohighlight">
<Combobox.Label class={styles.Label}>Department</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Engineering" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No results found</Combobox.Empty>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Inline Autocomplete
Complete the input value with the first matching item by setting inputBehavior="autocomplete". Use with
startsWith filter for best results.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
export const InlineAutocomplete = () => {
const { startsWith } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Whale', value: 'whale' },
{ label: 'Dolphin', value: 'dolphin' },
{ label: 'Shark', value: 'shark' },
{ label: 'Octopus', value: 'octopus' },
{ label: 'Jellyfish', value: 'jellyfish' },
{ label: 'Seahorse', value: 'seahorse' },
],
filter: startsWith,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
className={styles.Root}
collection={collection}
onInputValueChange={handleInputChange}
inputBehavior="autocomplete"
>
<Combobox.Label className={styles.Label}>Sea Creature</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Dolphin" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
<Combobox.Empty className={styles.Item}>No results found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</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'
import styles from 'styles/combobox.module.css'
export const InlineAutocomplete = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Whale', value: 'whale' },
{ label: 'Dolphin', value: 'dolphin' },
{ label: 'Shark', value: 'shark' },
{ label: 'Octopus', value: 'octopus' },
{ label: 'Jellyfish', value: 'jellyfish' },
{ label: 'Seahorse', value: 'seahorse' },
],
filter: filterFn().startsWith,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
class={styles.Root}
collection={collection()}
onInputValueChange={handleInputChange}
inputBehavior="autocomplete"
>
<Combobox.Label class={styles.Label}>Sea Creature</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Dolphin" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No results found</Combobox.Empty>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.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'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Whale', value: 'whale' },
{ label: 'Dolphin', value: 'dolphin' },
{ label: 'Shark', value: 'shark' },
{ label: 'Octopus', value: 'octopus' },
{ label: 'Jellyfish', value: 'jellyfish' },
{ label: 'Seahorse', value: 'seahorse' },
],
filter: filters.value.startsWith,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root
:class="styles.Root"
:collection="collection"
@input-value-change="handleInputChange"
input-behavior="autocomplete"
>
<Combobox.Label :class="styles.Label">Sea Creature</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Dolphin" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Empty :class="styles.Item">No results found</Combobox.Empty>
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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 { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'Whale', value: 'whale' },
{ label: 'Dolphin', value: 'dolphin' },
{ label: 'Shark', value: 'shark' },
{ label: 'Octopus', value: 'octopus' },
{ label: 'Jellyfish', value: 'jellyfish' },
{ label: 'Seahorse', value: 'seahorse' },
],
filter(itemString, filterText) {
return filters().startsWith(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange} inputBehavior="autocomplete">
<Combobox.Label class={styles.Label}>Sea Creature</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Dolphin" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No results found</Combobox.Empty>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</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'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
export const Grouping = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: contains,
groupBy: (item) => item.continent,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root className={styles.Root} collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label className={styles.Label}>Country</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Canada" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.group().map(([continent, group]) => (
<Combobox.ItemGroup className={styles.ItemGroup} key={continent}>
<Combobox.ItemGroupLabel className={styles.ItemGroupLabel}>{continent}</Combobox.ItemGroupLabel>
{group.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const initialItems = [
{ label: 'Canada', value: 'ca', continent: 'North America' },
{ label: 'United States', value: 'us', continent: 'North America' },
{ label: 'Mexico', value: 'mx', continent: 'North America' },
{ label: 'United Kingdom', value: 'uk', continent: 'Europe' },
{ label: 'Germany', value: 'de', continent: 'Europe' },
{ label: 'France', value: 'fr', continent: 'Europe' },
{ label: 'Japan', value: 'jp', continent: 'Asia' },
{ label: 'South Korea', value: 'kr', continent: 'Asia' },
{ label: 'China', value: 'cn', continent: 'Asia' },
]
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-solid'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
import styles from 'styles/combobox.module.css'
const initialItems = [
{ label: 'Canada', value: 'ca', continent: 'North America' },
{ label: 'United States', value: 'us', continent: 'North America' },
{ label: 'Mexico', value: 'mx', continent: 'North America' },
{ label: 'United Kingdom', value: 'uk', continent: 'Europe' },
{ label: 'Germany', value: 'de', continent: 'Europe' },
{ label: 'France', value: 'fr', continent: 'Europe' },
{ label: 'Japan', value: 'jp', continent: 'Asia' },
{ label: 'South Korea', value: 'kr', continent: 'Asia' },
{ label: 'China', value: 'cn', continent: 'Asia' },
]
export const Grouping = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
groupBy: (item) => item.continent,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root class={styles.Root} collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Country</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Canada" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().group()}>
{([continent, group]) => (
<Combobox.ItemGroup class={styles.ItemGroup}>
<Combobox.ItemGroupLabel class={styles.ItemGroupLabel}>{continent}</Combobox.ItemGroupLabel>
<For each={group}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
)}
</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'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
const initialItems = [
{ label: 'Canada', value: 'ca', continent: 'North America' },
{ label: 'United States', value: 'us', continent: 'North America' },
{ label: 'Mexico', value: 'mx', continent: 'North America' },
{ label: 'Germany', value: 'de', continent: 'Europe' },
{ label: 'France', value: 'fr', continent: 'Europe' },
{ label: 'United Kingdom', value: 'uk', continent: 'Europe' },
{ label: 'Japan', value: 'jp', continent: 'Asia' },
{ label: 'China', value: 'cn', continent: 'Asia' },
{ label: 'India', value: 'in', continent: 'Asia' },
]
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
groupBy: (item) => item.continent,
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :class="styles.Root" :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label :class="styles.Label">Country</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Canada" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.ItemGroup :key="continent" v-for="[continent, group] in collection.group()">
<Combobox.ItemGroupLabel :class="styles.ItemGroupLabel">{{ continent }}</Combobox.ItemGroupLabel>
<Combobox.Item v-for="item in group" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'Canada', value: 'ca', continent: 'North America' },
{ label: 'United States', value: 'us', continent: 'North America' },
{ label: 'Mexico', value: 'mx', continent: 'North America' },
{ label: 'Germany', value: 'de', continent: 'Europe' },
{ label: 'France', value: 'fr', continent: 'Europe' },
{ label: 'United Kingdom', value: 'uk', continent: 'Europe' },
{ label: 'Japan', value: 'jp', continent: 'Asia' },
{ label: 'China', value: 'cn', continent: 'Asia' },
{ label: 'India', value: 'in', continent: 'Asia' },
]
const { collection, filter } = useListCollection({
initialItems,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
groupBy: (item) => item.continent,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Country</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Canada" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().group() as [continent, group]}
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel class={styles.ItemGroupLabel}>{continent}</Combobox.ItemGroupLabel>
{#each group as item}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.ItemGroup>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
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'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
import field from 'styles/field.module.css'
export const WithField = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Field.Root className={field.Root}>
<Combobox.Root className={styles.Root} collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label className={styles.Label}>Department</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Engineering" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText className={field.HelperText}>Select your primary department</Field.HelperText>
<Field.ErrorText className={field.ErrorText}>Department is required</Field.ErrorText>
</Field.Root>
)
}
const initialItems = [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Design', value: 'design' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Finance', value: 'finance' },
]
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'
import styles from 'styles/combobox.module.css'
import field from 'styles/field.module.css'
const initialItems = [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Design', value: 'design' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Finance', value: 'finance' },
]
export const WithField = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Field.Root class={field.Root}>
<Combobox.Root class={styles.Root} collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Department</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Engineering" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText class={field.HelperText}>Select your primary department</Field.HelperText>
<Field.ErrorText class={field.ErrorText}>Department is required</Field.ErrorText>
</Field.Root>
)
}
<script setup lang="ts">
// biome-ignore lint/style/useImportType: intentional
import { Combobox, useListCollection } from '@ark-ui/vue/combobox'
import { Field } from '@ark-ui/vue/field'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
import field from 'styles/field.module.css'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Design', value: 'design' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Finance', value: 'finance' },
]
const { collection, filter } = useListCollection({
initialItems,
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Field.Root :class="field.Root">
<Combobox.Root :class="styles.Root" :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label :class="styles.Label">Department</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Engineering" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.ItemIndicator">✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText :class="field.HelperText">Select your primary department</Field.HelperText>
<Field.ErrorText :class="field.ErrorText">Department is required</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'
import styles from 'styles/combobox.module.css'
import field from 'styles/field.module.css'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'Engineering', value: 'engineering' },
{ label: 'Design', value: 'design' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Sales', value: 'sales' },
{ label: 'Human Resources', value: 'hr' },
{ label: 'Finance', value: 'finance' },
]
const { collection, filter } = useListCollection({
initialItems,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Field.Root class={field.Root}>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Department</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Engineering" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
<Field.HelperText class={field.HelperText}>Select your primary department</Field.HelperText>
<Field.ErrorText class={field.ErrorText}>Department is required</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'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import button from 'styles/button.module.css'
import styles from 'styles/combobox.module.css'
const initialItems = [
{ label: 'Designer', value: 'designer' },
{ label: 'Developer', value: 'developer' },
{ label: 'Product Manager', value: 'pm' },
{ label: 'Data Scientist', value: 'data-scientist' },
{ label: 'DevOps Engineer', value: 'devops' },
{ label: 'Marketing Lead', value: 'marketing' },
]
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 (
<div className="stack">
<button className={button.Root} onClick={() => combobox.focus()}>
Focus
</button>
<Combobox.RootProvider className={styles.Root} value={combobox}>
<Combobox.Label className={styles.Label}>Job Title</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Designer" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
</div>
)
}
import { Combobox, useCombobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-solid'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
import button from 'styles/button.module.css'
import styles from 'styles/combobox.module.css'
const initialItems = [
{ label: 'Designer', value: 'designer' },
{ label: 'Developer', value: 'developer' },
{ label: 'Product Manager', value: 'pm' },
{ label: 'Data Scientist', value: 'data-scientist' },
{ label: 'DevOps Engineer', value: 'devops' },
{ label: 'Marketing Lead', value: 'marketing' },
]
export const RootProvider = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems,
filter: filterFn().contains,
})
const combobox = useCombobox({
get collection() {
return collection()
},
onInputValueChange(details) {
filter(details.inputValue)
},
})
return (
<div class="stack">
<button class={button.Root} onClick={() => combobox().focus()}>
Focus
</button>
<Combobox.RootProvider class={styles.Root} value={combobox}>
<Combobox.Label class={styles.Label}>Job Title</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Designer" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
</div>
)
}
<script setup lang="ts">
import { Combobox, useCombobox, useListCollection } from '@ark-ui/vue/combobox'
import { useFilter } from '@ark-ui/vue/locale'
import button from 'styles/button.module.css'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'Designer', value: 'designer' },
{ label: 'Developer', value: 'developer' },
{ label: 'Product Manager', value: 'pm' },
{ label: 'Data Scientist', value: 'data-scientist' },
{ label: 'DevOps Engineer', value: 'devops' },
{ label: 'Marketing Lead', value: 'marketing' },
]
const { collection, filter } = useListCollection({
initialItems,
filter: filters.value.contains,
})
const combobox = useCombobox({
get collection() {
return collection.value
},
onInputValueChange(details) {
filter(details.inputValue)
},
})
</script>
<template>
<div class="stack">
<button :class="button.Root" @click="combobox.focus()">Focus</button>
<Combobox.RootProvider :class="styles.Root" :value="combobox">
<Combobox.Label :class="styles.Label">Job Title</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Designer" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.ItemIndicator">✓</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.RootProvider>
</div>
</template>
<script lang="ts">
import { Combobox, useCombobox, useListCollection } from '@ark-ui/svelte/combobox'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import button from 'styles/button.module.css'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'Designer', value: 'designer' },
{ label: 'Developer', value: 'developer' },
{ label: 'Product Manager', value: 'pm' },
{ label: 'Data Scientist', value: 'data-scientist' },
{ label: 'DevOps Engineer', value: 'devops' },
{ label: 'Marketing Lead', value: 'marketing' },
]
const { collection, filter } = useListCollection({
initialItems,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const id = $props.id()
const combobox = useCombobox({
get collection() {
return collection()
},
id,
onInputValueChange(details) {
filter(details.inputValue)
},
})
</script>
<div class="stack">
<button class={button.Root} onclick={() => combobox().focus()}>
Focus
</button>
<Combobox.RootProvider class={styles.Root} value={combobox}>
<Combobox.Label class={styles.Label}>Job Title</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Designer" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
</div>
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'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
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
className={styles.Root}
collection={collection}
onInputValueChange={handleInputChange}
selectionBehavior="preserve"
>
<Combobox.Label className={styles.Label}>Developer Resources</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. GitHub" />
<div className={styles.Indicators}>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item} asChild>
<a href={item.href}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</a>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const initialItems = [
{ label: 'GitHub', href: 'https://github.com', value: 'github' },
{ label: 'Stack Overflow', href: 'https://stackoverflow.com', value: 'stackoverflow' },
{ label: 'MDN Web Docs', href: 'https://developer.mozilla.org', value: 'mdn' },
{ label: 'npm', href: 'https://www.npmjs.com', value: 'npm' },
{ label: 'TypeScript', href: 'https://www.typescriptlang.org', value: 'typescript' },
{ label: 'React', href: 'https://react.dev', value: 'react' },
]
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-solid'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
import styles from 'styles/combobox.module.css'
const initialItems = [
{ label: 'GitHub', href: 'https://github.com', value: 'github' },
{ label: 'Stack Overflow', href: 'https://stackoverflow.com', value: 'stackoverflow' },
{ label: 'MDN Web Docs', href: 'https://developer.mozilla.org', value: 'mdn' },
{ label: 'npm', href: 'https://www.npmjs.com', value: 'npm' },
{ label: 'TypeScript', href: 'https://www.typescriptlang.org', value: 'typescript' },
{ label: 'React', href: 'https://react.dev', value: 'react' },
]
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
class={styles.Root}
collection={collection()}
onInputValueChange={handleInputChange}
selectionBehavior="preserve"
>
<Combobox.Label class={styles.Label}>Developer Resources</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. GitHub" />
<div class={styles.Indicators}>
<Combobox.Trigger class={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item} asChild={(props) => <a href={item.href} {...props()} />}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</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'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
const initialItems = [
{ label: 'GitHub', href: 'https://github.com', value: 'github' },
{ label: 'Stack Overflow', href: 'https://stackoverflow.com', value: 'stackoverflow' },
{ label: 'MDN Web Docs', href: 'https://developer.mozilla.org', value: 'mdn' },
{ label: 'Dev.to', href: 'https://dev.to', value: 'devto' },
{ label: 'Hacker News', href: 'https://news.ycombinator.com', value: 'hackernews' },
{ label: 'Reddit Programming', href: 'https://reddit.com/r/programming', value: 'reddit' },
]
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
:class="styles.Root"
:collection="collection"
@input-value-change="handleInputChange"
selection-behavior="preserve"
>
<Combobox.Label :class="styles.Label">Developer Resources</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. GitHub" />
<div :class="styles.Indicators">
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Item
v-for="item in collection.items"
:key="item.value"
:item="item"
:class="styles.Item"
:as-child="true"
>
<a :href="item.href">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const initialItems = [
{ label: 'GitHub', href: 'https://github.com', value: 'github' },
{ label: 'Stack Overflow', href: 'https://stackoverflow.com', value: 'stackoverflow' },
{ label: 'MDN Web Docs', href: 'https://developer.mozilla.org', value: 'mdn' },
{ label: 'Dev.to', href: 'https://dev.to', value: 'devto' },
{ label: 'Hacker News', href: 'https://news.ycombinator.com', value: 'hackernews' },
{ label: 'Reddit Programming', href: 'https://reddit.com/r/programming', value: 'reddit' },
]
const { collection, filter } = useListCollection({
initialItems,
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange} selectionBehavior="preserve">
<Combobox.Label class={styles.Label}>Developer Resources</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. GitHub" />
<div class={styles.Indicators}>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
{#snippet asChild(props)}
<a {...props()} href={item.href}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</a>
{/snippet}
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Rehydrate Value
When a combobox has a defaultValue or value but the collection is not loaded yet, you can rehydrate the value to
populate the input.
import { Combobox, useCombobox, useComboboxContext, useListCollection } from '@ark-ui/react/combobox'
import { Portal } from '@ark-ui/react/portal'
import { CheckIcon } from 'lucide-react'
import { useRef, useState } from 'react'
import { useAsync } from 'react-use'
import styles from 'styles/combobox.module.css'
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 className={styles.Root} value={combobox}>
<Combobox.Label className={styles.Label}>Search Star Wars Characters</Combobox.Label>
<ComboboxRehydrateValue />
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Luke" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{state.loading ? (
<span style={{ padding: '0.5rem' }}>Loading...</span>
) : state.error ? (
<span style={{ padding: '0.5rem' }}>{state.error.message}</span>
) : (
collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.name} item={item}>
<Combobox.ItemText className={styles.ItemText}>
{item.name} - {item.height}cm / {item.mass}kg
</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</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 styles from 'styles/combobox.module.css'
import { useAsync } from './use-async'
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 class={styles.Root} value={combobox}>
<Combobox.Label class={styles.Label}>Search Star Wars Characters</Combobox.Label>
<ComboboxRehydrateValue />
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Luke" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{state.loading() ? (
<span style={{ padding: '0.5rem' }}>Loading...</span>
) : state.error() ? (
<span style={{ padding: '0.5rem' }}>{state.error()?.message}</span>
) : (
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>
{item.name} - {item.height}cm / {item.mass}kg
</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</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 styles from 'styles/combobox.module.css'
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
}
let hydrated = false
watchEffect(() => {
if (combobox.value.value.length && combobox.value.collection.size && !hydrated) {
combobox.value.syncSelectedItems()
hydrated = true
}
})
</script>
<template>
<Combobox.RootProvider :class="styles.Root" :value="combobox">
<Combobox.Label :class="styles.Label">Search Star Wars Characters</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Luke" />
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<span v-if="state.loading.value" style="padding: 0.5rem">Loading...</span>
<span v-else-if="state.error.value" style="padding: 0.5rem">{{ state.error.value.message }}</span>
<template v-else>
<Combobox.Item v-for="item in collection.items" :key="item.name" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">
{{ item.name }} - {{ item.height }}cm / {{ item.mass }}kg
</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.ItemIndicator">✓</Combobox.ItemIndicator>
</Combobox.Item>
</template>
</Combobox.Content>
</Combobox.Positioner>
</Teleport>
</Combobox.RootProvider>
</template>
<script lang="ts">
import { Combobox, useCombobox, useListCollection } from '@ark-ui/svelte/combobox'
import { Portal } from '@ark-ui/svelte/portal'
import styles from 'styles/combobox.module.css'
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()
})
let hydrated = false
$effect.pre(() => {
if (combobox().value.length && combobox().collection.size && !hydrated) {
combobox().syncSelectedItems()
hydrated = true
}
})
</script>
<Combobox.RootProvider class={styles.Root} value={combobox}>
<Combobox.Label class={styles.Label}>Search Star Wars Characters</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Luke" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#if _state.loading()}
<span style="padding: 0.5rem">Loading...</span>
{:else if _state.error()}
<span style="padding: 0.5rem">{_state.error()?.message}</span>
{:else}
{#each collection().items as item (item.name)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>
{item.name} - {item.height}cm / {item.mass}kg
</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
{/if}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
Highlight Matching Text
Highlight the matching 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'
import { ChevronsUpDownIcon, XIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
export const HighlightMatchingText = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'John Smith', value: 'john-smith' },
{ label: 'Jane Doe', value: 'jane-doe' },
{ label: 'Bob Johnson', value: 'bob-johnson' },
{ label: 'Alice Williams', value: 'alice-williams' },
{ label: 'Charlie Brown', value: 'charlie-brown' },
{ label: 'Diana Ross', value: 'diana-ross' },
],
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root className={styles.Root} collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label className={styles.Label}>Assignee</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. John Smith" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>
<Combobox.Context>
{(context) => <Highlight text={item.label} query={context.inputValue} ignoreCase />}
</Combobox.Context>
</Combobox.ItemText>
</Combobox.Item>
))}
</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'
import styles from 'styles/combobox.module.css'
export const HighlightMatchingText = () => {
const filterFn = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'John Smith', value: 'john-smith' },
{ label: 'Jane Doe', value: 'jane-doe' },
{ label: 'Bob Johnson', value: 'bob-johnson' },
{ label: 'Alice Williams', value: 'alice-williams' },
{ label: 'Charlie Brown', value: 'charlie-brown' },
{ label: 'Diana Ross', value: 'diana-ross' },
],
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root class={styles.Root} collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Assignee</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. John Smith" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>
<Combobox.Context>
{(context) => <Highlight text={item.label} query={context().inputValue} ignoreCase />}
</Combobox.Context>
</Combobox.ItemText>
</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'
import { Highlight } from '@ark-ui/vue/highlight'
import { useFilter } from '@ark-ui/vue/locale'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'John Smith', value: 'john-smith' },
{ label: 'Jane Doe', value: 'jane-doe' },
{ label: 'Bob Johnson', value: 'bob-johnson' },
{ label: 'Alice Williams', value: 'alice-williams' },
{ label: 'Charlie Brown', value: 'charlie-brown' },
{ label: 'Diana Ross', value: 'diana-ross' },
],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<template>
<Combobox.Root :class="styles.Root" :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label :class="styles.Label">Assignee</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. John Smith" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">
<Combobox.Context v-slot="context">
<Highlight :text="item.label" :query="context.inputValue" ignore-case />
</Combobox.Context>
</Combobox.ItemText>
</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 { Highlight } from '@ark-ui/svelte/highlight'
import { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const { collection, filter } = useListCollection({
initialItems: [
{ label: 'John Smith', value: 'john-smith' },
{ label: 'Jane Doe', value: 'jane-doe' },
{ label: 'Bob Johnson', value: 'bob-johnson' },
{ label: 'Alice Williams', value: 'alice-williams' },
{ label: 'Charlie Brown', value: 'charlie-brown' },
{ label: 'Diana Ross', value: 'diana-ross' },
],
filter(itemString, filterText) {
return filters().contains(itemString, filterText)
},
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
</script>
<Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Assignee</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. John Smith" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>
<Combobox.Context>
{#snippet render(context)}
<Highlight text={item.label} query={context().inputValue} ignoreCase />
{/snippet}
</Combobox.Context>
</Combobox.ItemText>
</Combobox.Item>
{/each}
</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'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import styles from 'styles/combobox.module.css'
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 className={styles.Root} collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label className={styles.Label}>Email</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. john" />
<div className={styles.Indicators}>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</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'
import styles from 'styles/combobox.module.css'
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 class={styles.Root} collection={collection()} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Email</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. john" />
<div class={styles.Indicators}>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.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'
import styles from 'styles/combobox.module.css'
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 :class="styles.Root" :collection="collection" @input-value-change="handleInputChange">
<Combobox.Label :class="styles.Label">Email</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. john" />
<div :class="styles.Indicators">
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Item v-for="item in collection.items" :key="item" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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'
import styles from 'styles/combobox.module.css'
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 class={styles.Root} {collection} onInputValueChange={handleInputChange}>
<Combobox.Label class={styles.Label}>Email</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. john" />
<div class={styles.Indicators}>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().items as item (item)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.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 { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import { useState } from 'react'
import { flushSync } from 'react-dom'
import styles from 'styles/combobox.module.css'
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: 'Bug', value: 'bug' },
{ label: 'Feature', value: 'feature' },
{ label: 'Enhancement', value: 'enhancement' },
{ label: 'Documentation', value: 'docs' },
],
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
className={styles.Root}
collection={collection}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
value={selectedValue}
onValueChange={handleValueChange}
allowCustomValue
>
<Combobox.Label className={styles.Label}>Label</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Bug" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
{isNewOptionValue(item.value) ? (
<Combobox.ItemText className={styles.ItemText}>+ Create "{item.label}"</Combobox.ItemText>
) : (
<Combobox.ItemText className={styles.ItemText}>
{item.label} {item.__new__ ? '(new)' : ''}
</Combobox.ItemText>
)}
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</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'
import styles from 'styles/combobox.module.css'
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: 'Bug', value: 'bug' },
{ label: 'Feature', value: 'feature' },
{ label: 'Enhancement', value: 'enhancement' },
{ label: 'Documentation', value: 'docs' },
],
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
class={styles.Root}
collection={collection()}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
value={selectedValue()}
onValueChange={handleValueChange}
allowCustomValue
>
<Combobox.Label class={styles.Label}>Label</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Bug" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
{isNewOptionValue(item.value) ? (
<Combobox.ItemText class={styles.ItemText}>+ Create "{item.label}"</Combobox.ItemText>
) : (
<Combobox.ItemText class={styles.ItemText}>
{item.label} {item.__new__ ? '(new)' : ''}
</Combobox.ItemText>
)}
<Combobox.ItemIndicator class={styles.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'
import { useFilter } from '@ark-ui/vue/locale'
import { nextTick, ref } from 'vue'
import styles from 'styles/combobox.module.css'
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: 'Bug', value: 'bug' },
{ label: 'Feature', value: 'feature' },
{ label: 'Enhancement', value: 'enhancement' },
{ label: 'Documentation', value: 'docs' },
],
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
:class="styles.Root"
:collection="collection"
:model-value="selectedValue"
:onInputValueChange="handleInputChange"
:onOpenChange="handleOpenChange"
:onValueChange="handleValueChange"
allowCustomValue
>
<Combobox.Label :class="styles.Label">Label</Combobox.Label>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. Bug" />
<div :class="styles.Indicators">
<Combobox.ClearTrigger :class="styles.ClearTrigger">Clear</Combobox.ClearTrigger>
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText v-if="isNewOptionValue(item.value)" :class="styles.ItemText">
+ Create "{{ item.label }}"
</Combobox.ItemText>
<Combobox.ItemText v-else :class="styles.ItemText">
{{ item.label }} {{ item.__new__ ? '(new)' : '' }}
</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.ItemIndicator">✓</Combobox.ItemIndicator>
</Combobox.Item>
</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'
import styles from 'styles/combobox.module.css'
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: 'Bug', value: 'bug' },
{ label: 'Feature', value: 'feature' },
{ label: 'Enhancement', value: 'enhancement' },
{ label: 'Documentation', value: 'docs' },
],
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
class={styles.Root}
{collection}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
value={selectedValue}
onValueChange={handleValueChange}
allowCustomValue
>
<Combobox.Label class={styles.Label}>Label</Combobox.Label>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. Bug" />
<div class={styles.Indicators}>
<Combobox.ClearTrigger class={styles.ClearTrigger}>Clear</Combobox.ClearTrigger>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
{#if isNewOptionValue(item.value)}
<Combobox.ItemText class={styles.ItemText}>+ Create "{item.label}"</Combobox.ItemText>
{:else}
<Combobox.ItemText class={styles.ItemText}>
{item.label} {item.__new__ ? '(new)' : ''}
</Combobox.ItemText>
{/if}
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Multiple Selection
Enable multiple selection by setting the multiple prop. Selected items can be displayed as tags above the input.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import { useRef } from 'react'
import styles from 'styles/combobox.module.css'
export const Multiple = () => {
const { contains } = useFilter({ sensitivity: 'base' })
const selectedValue = useRef<string[]>([])
const { collection, filter, remove } = useListCollection({
initialItems: [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Python', value: 'python' },
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'Java', value: 'java' },
],
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
const handleValueChange = (details: Combobox.ValueChangeDetails) => {
selectedValue.current = details.value
remove(...details.value)
}
return (
<Combobox.Root
className={styles.Root}
collection={collection}
onInputValueChange={handleInputChange}
onValueChange={handleValueChange}
multiple
>
<Combobox.Label className={styles.Label}>Skills</Combobox.Label>
<Combobox.Context>
{(combobox) => (
<div className={styles.Tags}>
{combobox.selectedItems.length === 0 && <span className={styles.TagPlaceholder}>None selected</span>}
{combobox.selectedItems.map((item: any) => (
<span key={item.value} className={styles.Tag}>
{item.label}
</span>
))}
</div>
)}
</Combobox.Context>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. JavaScript" />
<div className={styles.Indicators}>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
<Combobox.Empty className={styles.Item}>No skills found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item className={styles.Item} key={item.value} item={item}>
<Combobox.ItemText className={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
import { Combobox, useListCollection } from '@ark-ui/solid/combobox'
import { useFilter } from '@ark-ui/solid/locale'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-solid'
import { For } from 'solid-js'
import { Portal } from 'solid-js/web'
import styles from 'styles/combobox.module.css'
export const Multiple = () => {
const filterFn = useFilter({ sensitivity: 'base' })
let selectedValue: string[] = []
const { collection, filter, remove } = useListCollection({
initialItems: [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Python', value: 'python' },
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'Java', value: 'java' },
],
filter: filterFn().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
const handleValueChange = (details: Combobox.ValueChangeDetails) => {
selectedValue = details.value
remove(...details.value)
}
return (
<Combobox.Root
class={styles.Root}
collection={collection()}
onInputValueChange={handleInputChange}
onValueChange={handleValueChange}
multiple
>
<Combobox.Label class={styles.Label}>Skills</Combobox.Label>
<Combobox.Context>
{(context) => (
<div class={styles.Tags}>
{context().selectedItems.length === 0 && <span class={styles.TagPlaceholder}>None selected</span>}
<For each={context().selectedItems}>{(item: any) => <span class={styles.Tag}>{item.label}</span>}</For>
</div>
)}
</Combobox.Context>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. JavaScript" />
<div class={styles.Indicators}>
<Combobox.Trigger class={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No skills found</Combobox.Empty>
<For each={collection().items}>
{(item) => (
<Combobox.Item class={styles.Item} item={item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</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'
import { useFilter } from '@ark-ui/vue/locale'
import { ref } from 'vue'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
const selectedItems = ref<{ label: string; value: string }[]>([])
const { collection, filter, remove } = useListCollection({
initialItems: [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Python', value: 'python' },
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'Java', value: 'java' },
],
filter: filters.value.contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
const handleValueChange = (details: Combobox.ValueChangeDetails) => {
selectedItems.value = details.items
remove(...details.value)
}
</script>
<template>
<Combobox.Root
:class="styles.Root"
:collection="collection"
@input-value-change="handleInputChange"
@value-change="handleValueChange"
multiple
>
<Combobox.Label :class="styles.Label">Skills</Combobox.Label>
<div :class="styles.Tags">
<span v-if="selectedItems.length === 0" :class="styles.TagPlaceholder">None selected</span>
<span v-for="item in selectedItems" :key="item.value" :class="styles.Tag">{{ item.label }}</span>
</div>
<Combobox.Control :class="styles.Control">
<Combobox.Input :class="styles.Input" placeholder="e.g. JavaScript" />
<div :class="styles.Indicators">
<Combobox.Trigger :class="styles.Trigger">Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Teleport to="body">
<Combobox.Positioner>
<Combobox.Content :class="styles.Content">
<Combobox.Empty :class="styles.Item">No skills found</Combobox.Empty>
<Combobox.Item v-for="item in collection.items" :key="item.value" :item="item" :class="styles.Item">
<Combobox.ItemText :class="styles.ItemText">{{ item.label }}</Combobox.ItemText>
<Combobox.ItemIndicator :class="styles.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 { useFilter } from '@ark-ui/svelte/locale'
import { Portal } from '@ark-ui/svelte/portal'
import styles from 'styles/combobox.module.css'
const filters = useFilter({ sensitivity: 'base' })
let selectedItems: { label: string; value: string }[] = $state([])
const { collection, filter, remove } = useListCollection({
initialItems: [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Python', value: 'python' },
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'Java', value: 'java' },
],
filter: filters().contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
const handleValueChange = (details: Combobox.ValueChangeDetails) => {
selectedItems = details.items
remove(...details.value)
}
</script>
<Combobox.Root
class={styles.Root}
{collection}
onInputValueChange={handleInputChange}
onValueChange={handleValueChange}
multiple
>
<Combobox.Label class={styles.Label}>Skills</Combobox.Label>
<div class={styles.Tags}>
{#if selectedItems.length === 0}
<span class={styles.TagPlaceholder}>None selected</span>
{/if}
{#each selectedItems as item (item.value)}
<span class={styles.Tag}>{item.label}</span>
{/each}
</div>
<Combobox.Control class={styles.Control}>
<Combobox.Input class={styles.Input} placeholder="e.g. JavaScript" />
<div class={styles.Indicators}>
<Combobox.Trigger class={styles.Trigger}>Open</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content class={styles.Content}>
<Combobox.Empty class={styles.Item}>No skills found</Combobox.Empty>
{#each collection().items as item (item.value)}
<Combobox.Item class={styles.Item} {item}>
<Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator class={styles.ItemIndicator}>✓</Combobox.ItemIndicator>
</Combobox.Item>
{/each}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
Async Search
Load options asynchronously based on user input using the useAsyncList hook. This is useful for searching large
datasets or fetching data from an API.
import { useAsyncList } from '@ark-ui/react/collection'
import { Combobox, createListCollection } from '@ark-ui/react/combobox'
import { Portal } from '@ark-ui/react/portal'
import { CheckIcon, ChevronsUpDownIcon, LoaderIcon, XIcon } from 'lucide-react'
import { startTransition } from 'react'
import styles from 'styles/combobox.module.css'
interface Movie {
id: string
title: string
year: number
director: string
genre: string
}
export const AsyncSearch = () => {
const list = useAsyncList<Movie>({
async load({ filterText, signal }) {
if (!filterText) return { items: [] }
await new Promise((resolve) => setTimeout(resolve, 300))
if (signal?.aborted) return { items: [] }
const items = allMovies.filter(
(movie) =>
movie.title.toLowerCase().includes(filterText.toLowerCase()) ||
movie.director.toLowerCase().includes(filterText.toLowerCase()) ||
movie.genre.toLowerCase().includes(filterText.toLowerCase()),
)
return { items }
},
})
const collection = createListCollection({
items: list.items,
itemToString: (item) => item.title,
itemToValue: (item) => item.id,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
if (details.reason === 'input-change') {
startTransition(() => {
list.setFilterText(details.inputValue)
})
}
}
return (
<Combobox.Root className={styles.Root} collection={collection} onInputValueChange={handleInputChange}>
<Combobox.Label className={styles.Label}>Movie</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Inception" />
<div className={styles.Indicators}>
<Combobox.ClearTrigger className={styles.ClearTrigger}>
<XIcon />
</Combobox.ClearTrigger>
<Combobox.Trigger className={styles.Trigger}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
{list.loading ? (
<div className={styles.Status}>
<LoaderIcon className={styles.Spinner} />
<span>Searching...</span>
</div>
) : list.error ? (
<div className={styles.Status}>{list.error.message}</div>
) : list.items.length === 0 ? (
<div className={styles.Status}>
{list.filterText ? 'No results found' : 'Start typing to search movies...'}
</div>
) : (
collection.items.map((movie) => (
<Combobox.Item className={styles.Item} key={movie.id} item={movie}>
<Combobox.ItemText className={styles.ItemText}>
<span className={styles.ItemTitle}>{movie.title}</span>
<span className={styles.ItemSubtitle}>
{movie.year} · {movie.director}
</span>
</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))
)}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const allMovies: Movie[] = [
{ id: 'inception', title: 'Inception', year: 2010, director: 'Christopher Nolan', genre: 'Sci-Fi' },
{ id: 'the-dark-knight', title: 'The Dark Knight', year: 2008, director: 'Christopher Nolan', genre: 'Action' },
{ id: 'pulp-fiction', title: 'Pulp Fiction', year: 1994, director: 'Quentin Tarantino', genre: 'Crime' },
{ id: 'the-godfather', title: 'The Godfather', year: 1972, director: 'Francis Ford Coppola', genre: 'Crime' },
{ id: 'forrest-gump', title: 'Forrest Gump', year: 1994, director: 'Robert Zemeckis', genre: 'Drama' },
{ id: 'the-matrix', title: 'The Matrix', year: 1999, director: 'The Wachowskis', genre: 'Sci-Fi' },
{ id: 'interstellar', title: 'Interstellar', year: 2014, director: 'Christopher Nolan', genre: 'Sci-Fi' },
{ id: 'parasite', title: 'Parasite', year: 2019, director: 'Bong Joon-ho', genre: 'Thriller' },
{
id: 'the-shawshank-redemption',
title: 'The Shawshank Redemption',
year: 1994,
director: 'Frank Darabont',
genre: 'Drama',
},
{ id: 'fight-club', title: 'Fight Club', year: 1999, director: 'David Fincher', genre: 'Drama' },
{ id: 'goodfellas', title: 'Goodfellas', year: 1990, director: 'Martin Scorsese', genre: 'Crime' },
{
id: 'the-silence-of-the-lambs',
title: 'The Silence of the Lambs',
year: 1991,
director: 'Jonathan Demme',
genre: 'Thriller',
},
]
Example not foundExample not foundExample not foundVirtualized
For very large lists, use virtualization with @tanstack/react-virtual to render only the visible items.
import { Combobox, useListCollection } from '@ark-ui/react/combobox'
import { useFilter } from '@ark-ui/react/locale'
import { Portal } from '@ark-ui/react/portal'
import { useVirtualizer } from '@tanstack/react-virtual'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import { useRef } from 'react'
import { flushSync } from 'react-dom'
import styles from 'styles/combobox.module.css'
export const Virtualized = () => {
const contentRef = useRef<HTMLDivElement | null>(null)
const { startsWith } = useFilter({ sensitivity: 'base' })
const { collection, filter, reset } = useListCollection({
initialItems: countries,
filter: startsWith,
})
const virtualizer = useVirtualizer({
count: collection.size,
getScrollElement: () => contentRef.current,
estimateSize: () => 32,
overscan: 10,
})
const handleScrollToIndex: Combobox.RootProps<Country>['scrollToIndexFn'] = (details) => {
flushSync(() => {
virtualizer.scrollToIndex(details.index, {
align: 'center',
behavior: 'auto',
})
})
}
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
className={styles.Root}
collection={collection}
onInputValueChange={handleInputChange}
scrollToIndexFn={handleScrollToIndex}
>
<Combobox.Label className={styles.Label}>Country</Combobox.Label>
<Combobox.Control className={styles.Control}>
<Combobox.Input className={styles.Input} placeholder="e.g. Germany" />
<div className={styles.Indicators}>
<Combobox.Trigger className={styles.Trigger} onClick={reset}>
<ChevronsUpDownIcon />
</Combobox.Trigger>
</div>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content className={styles.Content}>
<div
ref={contentRef}
className={styles.Scroller}
style={{ ['--total-size' as string]: `${virtualizer.getTotalSize()}px` }}
>
<div style={{ height: virtualizer.getTotalSize(), width: '100%', position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = collection.items[virtualItem.index]
return (
<Combobox.Item
className={styles.Item}
key={item.value}
item={item}
aria-setsize={collection.size}
aria-posinset={virtualItem.index + 1}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<Combobox.ItemText className={styles.ItemText}>
<span aria-hidden style={{ marginRight: 8 }}>
{item.emoji}
</span>
{item.label}
</Combobox.ItemText>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
)
})}
</div>
</div>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
interface Country {
value: string
label: string
emoji: string
}
const countries: Country[] = [
{ value: 'AD', label: 'Andorra', emoji: '🇦🇩' },
{ value: 'AE', label: 'United Arab Emirates', emoji: '🇦🇪' },
{ value: 'AF', label: 'Afghanistan', emoji: '🇦🇫' },
{ value: 'AG', label: 'Antigua and Barbuda', emoji: '🇦🇬' },
{ value: 'AI', label: 'Anguilla', emoji: '🇦🇮' },
{ value: 'AL', label: 'Albania', emoji: '🇦🇱' },
{ value: 'AM', label: 'Armenia', emoji: '🇦🇲' },
{ value: 'AO', label: 'Angola', emoji: '🇦🇴' },
{ value: 'AQ', label: 'Antarctica', emoji: '🇦🇶' },
{ value: 'AR', label: 'Argentina', emoji: '🇦🇷' },
{ value: 'AS', label: 'American Samoa', emoji: '🇦🇸' },
{ value: 'AT', label: 'Austria', emoji: '🇦🇹' },
{ value: 'AU', label: 'Australia', emoji: '🇦🇺' },
{ value: 'AW', label: 'Aruba', emoji: '🇦🇼' },
{ value: 'AX', label: 'Åland Islands', emoji: '🇦🇽' },
{ value: 'AZ', label: 'Azerbaijan', emoji: '🇦🇿' },
{ value: 'BA', label: 'Bosnia and Herzegovina', emoji: '🇧🇦' },
{ value: 'BB', label: 'Barbados', emoji: '🇧🇧' },
{ value: 'BD', label: 'Bangladesh', emoji: '🇧🇩' },
{ value: 'BE', label: 'Belgium', emoji: '🇧🇪' },
{ value: 'BF', label: 'Burkina Faso', emoji: '🇧🇫' },
{ value: 'BG', label: 'Bulgaria', emoji: '🇧🇬' },
{ value: 'BH', label: 'Bahrain', emoji: '🇧🇭' },
{ value: 'BI', label: 'Burundi', emoji: '🇧🇮' },
{ value: 'BJ', label: 'Benin', emoji: '🇧🇯' },
{ value: 'BL', label: 'Saint Barthélemy', emoji: '🇧🇱' },
{ value: 'BM', label: 'Bermuda', emoji: '🇧🇲' },
{ value: 'BN', label: 'Brunei', emoji: '🇧🇳' },
{ value: 'BO', label: 'Bolivia', emoji: '🇧🇴' },
{ value: 'BR', label: 'Brazil', emoji: '🇧🇷' },
{ value: 'BS', label: 'Bahamas', emoji: '🇧🇸' },
{ value: 'BT', label: 'Bhutan', emoji: '🇧🇹' },
{ value: 'BW', label: 'Botswana', emoji: '🇧🇼' },
{ value: 'BY', label: 'Belarus', emoji: '🇧🇾' },
{ value: 'BZ', label: 'Belize', emoji: '🇧🇿' },
{ value: 'CA', label: 'Canada', emoji: '🇨🇦' },
{ value: 'CD', label: 'Congo', emoji: '🇨🇩' },
{ value: 'CF', label: 'Central African Republic', emoji: '🇨🇫' },
{ value: 'CH', label: 'Switzerland', emoji: '🇨🇭' },
{ value: 'CI', label: "Côte d'Ivoire", emoji: '🇨🇮' },
{ value: 'CK', label: 'Cook Islands', emoji: '🇨🇰' },
{ value: 'CL', label: 'Chile', emoji: '🇨🇱' },
{ value: 'CM', label: 'Cameroon', emoji: '🇨🇲' },
{ value: 'CN', label: 'China', emoji: '🇨🇳' },
{ value: 'CO', label: 'Colombia', emoji: '🇨🇴' },
{ value: 'CR', label: 'Costa Rica', emoji: '🇨🇷' },
{ value: 'CU', label: 'Cuba', emoji: '🇨🇺' },
{ value: 'CV', label: 'Cabo Verde', emoji: '🇨🇻' },
{ value: 'CY', label: 'Cyprus', emoji: '🇨🇾' },
{ value: 'CZ', label: 'Czech Republic', emoji: '🇨🇿' },
{ value: 'DE', label: 'Germany', emoji: '🇩🇪' },
{ value: 'DJ', label: 'Djibouti', emoji: '🇩🇯' },
{ value: 'DK', label: 'Denmark', emoji: '🇩🇰' },
{ value: 'DM', label: 'Dominica', emoji: '🇩🇲' },
{ value: 'DO', label: 'Dominican Republic', emoji: '🇩🇴' },
{ value: 'DZ', label: 'Algeria', emoji: '🇩🇿' },
{ value: 'EC', label: 'Ecuador', emoji: '🇪🇨' },
{ value: 'EE', label: 'Estonia', emoji: '🇪🇪' },
{ value: 'EG', label: 'Egypt', emoji: '🇪🇬' },
{ value: 'ER', label: 'Eritrea', emoji: '🇪🇷' },
{ value: 'ES', label: 'Spain', emoji: '🇪🇸' },
{ value: 'ET', label: 'Ethiopia', emoji: '🇪🇹' },
{ value: 'FI', label: 'Finland', emoji: '🇫🇮' },
{ value: 'FJ', label: 'Fiji', emoji: '🇫🇯' },
{ value: 'FK', label: 'Falkland Islands', emoji: '🇫🇰' },
{ value: 'FM', label: 'Micronesia', emoji: '🇫🇲' },
{ value: 'FO', label: 'Faroe Islands', emoji: '🇫🇴' },
{ value: 'FR', label: 'France', emoji: '🇫🇷' },
{ value: 'GA', label: 'Gabon', emoji: '🇬🇦' },
{ value: 'GB', label: 'United Kingdom', emoji: '🇬🇧' },
{ value: 'GD', label: 'Grenada', emoji: '🇬🇩' },
{ value: 'GE', label: 'Georgia', emoji: '🇬🇪' },
{ value: 'GH', label: 'Ghana', emoji: '🇬🇭' },
{ value: 'GI', label: 'Gibraltar', emoji: '🇬🇮' },
{ value: 'GL', label: 'Greenland', emoji: '🇬🇱' },
{ value: 'GM', label: 'Gambia', emoji: '🇬🇲' },
{ value: 'GN', label: 'Guinea', emoji: '🇬🇳' },
{ value: 'GQ', label: 'Equatorial Guinea', emoji: '🇬🇶' },
{ value: 'GR', label: 'Greece', emoji: '🇬🇷' },
{ value: 'GT', label: 'Guatemala', emoji: '🇬🇹' },
{ value: 'GU', label: 'Guam', emoji: '🇬🇺' },
{ value: 'GW', label: 'Guinea-Bissau', emoji: '🇬🇼' },
{ value: 'GY', label: 'Guyana', emoji: '🇬🇾' },
{ value: 'HK', label: 'Hong Kong', emoji: '🇭🇰' },
{ value: 'HN', label: 'Honduras', emoji: '🇭🇳' },
{ value: 'HR', label: 'Croatia', emoji: '🇭🇷' },
{ value: 'HT', label: 'Haiti', emoji: '🇭🇹' },
{ value: 'HU', label: 'Hungary', emoji: '🇭🇺' },
{ value: 'ID', label: 'Indonesia', emoji: '🇮🇩' },
{ value: 'IE', label: 'Ireland', emoji: '🇮🇪' },
{ value: 'IL', label: 'Israel', emoji: '🇮🇱' },
{ value: 'IM', label: 'Isle of Man', emoji: '🇮🇲' },
{ value: 'IN', label: 'India', emoji: '🇮🇳' },
{ value: 'IQ', label: 'Iraq', emoji: '🇮🇶' },
{ value: 'IR', label: 'Iran', emoji: '🇮🇷' },
{ value: 'IS', label: 'Iceland', emoji: '🇮🇸' },
{ value: 'IT', label: 'Italy', emoji: '🇮🇹' },
{ value: 'JE', label: 'Jersey', emoji: '🇯🇪' },
{ value: 'JM', label: 'Jamaica', emoji: '🇯🇲' },
{ value: 'JO', label: 'Jordan', emoji: '🇯🇴' },
{ value: 'JP', label: 'Japan', emoji: '🇯🇵' },
{ value: 'KE', label: 'Kenya', emoji: '🇰🇪' },
{ value: 'KG', label: 'Kyrgyzstan', emoji: '🇰🇬' },
{ value: 'KH', label: 'Cambodia', emoji: '🇰🇭' },
{ value: 'KI', label: 'Kiribati', emoji: '🇰🇮' },
{ value: 'KM', label: 'Comoros', emoji: '🇰🇲' },
{ value: 'KN', label: 'Saint Kitts and Nevis', emoji: '🇰🇳' },
{ value: 'KP', label: 'North Korea', emoji: '🇰🇵' },
{ value: 'KR', label: 'South Korea', emoji: '🇰🇷' },
{ value: 'KW', label: 'Kuwait', emoji: '🇰🇼' },
{ value: 'KY', label: 'Cayman Islands', emoji: '🇰🇾' },
{ value: 'KZ', label: 'Kazakhstan', emoji: '🇰🇿' },
{ value: 'LA', label: 'Laos', emoji: '🇱🇦' },
{ value: 'LB', label: 'Lebanon', emoji: '🇱🇧' },
{ value: 'LC', label: 'Saint Lucia', emoji: '🇱🇨' },
{ value: 'LI', label: 'Liechtenstein', emoji: '🇱🇮' },
{ value: 'LK', label: 'Sri Lanka', emoji: '🇱🇰' },
{ value: 'LR', label: 'Liberia', emoji: '🇱🇷' },
{ value: 'LS', label: 'Lesotho', emoji: '🇱🇸' },
{ value: 'LT', label: 'Lithuania', emoji: '🇱🇹' },
{ value: 'LU', label: 'Luxembourg', emoji: '🇱🇺' },
{ value: 'LV', label: 'Latvia', emoji: '🇱🇻' },
{ value: 'LY', label: 'Libya', emoji: '🇱🇾' },
{ value: 'MA', label: 'Morocco', emoji: '🇲🇦' },
{ value: 'MC', label: 'Monaco', emoji: '🇲🇨' },
{ value: 'MD', label: 'Moldova', emoji: '🇲🇩' },
{ value: 'ME', label: 'Montenegro', emoji: '🇲🇪' },
{ value: 'MG', label: 'Madagascar', emoji: '🇲🇬' },
{ value: 'MH', label: 'Marshall Islands', emoji: '🇲🇭' },
{ value: 'MK', label: 'North Macedonia', emoji: '🇲🇰' },
{ value: 'ML', label: 'Mali', emoji: '🇲🇱' },
{ value: 'MM', label: 'Myanmar', emoji: '🇲🇲' },
{ value: 'MN', label: 'Mongolia', emoji: '🇲🇳' },
{ value: 'MO', label: 'Macao', emoji: '🇲🇴' },
{ value: 'MR', label: 'Mauritania', emoji: '🇲🇷' },
{ value: 'MS', label: 'Montserrat', emoji: '🇲🇸' },
{ value: 'MT', label: 'Malta', emoji: '🇲🇹' },
{ value: 'MU', label: 'Mauritius', emoji: '🇲🇺' },
{ value: 'MV', label: 'Maldives', emoji: '🇲🇻' },
{ value: 'MW', label: 'Malawi', emoji: '🇲🇼' },
{ value: 'MX', label: 'Mexico', emoji: '🇲🇽' },
{ value: 'MY', label: 'Malaysia', emoji: '🇲🇾' },
{ value: 'MZ', label: 'Mozambique', emoji: '🇲🇿' },
{ value: 'NA', label: 'Namibia', emoji: '🇳🇦' },
{ value: 'NC', label: 'New Caledonia', emoji: '🇳🇨' },
{ value: 'NE', label: 'Niger', emoji: '🇳🇪' },
{ value: 'NF', label: 'Norfolk Island', emoji: '🇳🇫' },
{ value: 'NG', label: 'Nigeria', emoji: '🇳🇬' },
{ value: 'NI', label: 'Nicaragua', emoji: '🇳🇮' },
{ value: 'NL', label: 'Netherlands', emoji: '🇳🇱' },
{ value: 'NO', label: 'Norway', emoji: '🇳🇴' },
{ value: 'NP', label: 'Nepal', emoji: '🇳🇵' },
{ value: 'NR', label: 'Nauru', emoji: '🇳🇷' },
{ value: 'NU', label: 'Niue', emoji: '🇳🇺' },
{ value: 'NZ', label: 'New Zealand', emoji: '🇳🇿' },
{ value: 'OM', label: 'Oman', emoji: '🇴🇲' },
{ value: 'PA', label: 'Panama', emoji: '🇵🇦' },
{ value: 'PE', label: 'Peru', emoji: '🇵🇪' },
{ value: 'PF', label: 'French Polynesia', emoji: '🇵🇫' },
{ value: 'PG', label: 'Papua New Guinea', emoji: '🇵🇬' },
{ value: 'PH', label: 'Philippines', emoji: '🇵🇭' },
{ value: 'PK', label: 'Pakistan', emoji: '🇵🇰' },
{ value: 'PL', label: 'Poland', emoji: '🇵🇱' },
{ value: 'PR', label: 'Puerto Rico', emoji: '🇵🇷' },
{ value: 'PS', label: 'Palestine', emoji: '🇵🇸' },
{ value: 'PT', label: 'Portugal', emoji: '🇵🇹' },
{ value: 'PW', label: 'Palau', emoji: '🇵🇼' },
{ value: 'PY', label: 'Paraguay', emoji: '🇵🇾' },
{ value: 'QA', label: 'Qatar', emoji: '🇶🇦' },
{ value: 'RO', label: 'Romania', emoji: '🇷🇴' },
{ value: 'RS', label: 'Serbia', emoji: '🇷🇸' },
{ value: 'RU', label: 'Russia', emoji: '🇷🇺' },
{ value: 'RW', label: 'Rwanda', emoji: '🇷🇼' },
{ value: 'SA', label: 'Saudi Arabia', emoji: '🇸🇦' },
{ value: 'SB', label: 'Solomon Islands', emoji: '🇸🇧' },
{ value: 'SC', label: 'Seychelles', emoji: '🇸🇨' },
{ value: 'SD', label: 'Sudan', emoji: '🇸🇩' },
{ value: 'SE', label: 'Sweden', emoji: '🇸🇪' },
{ value: 'SG', label: 'Singapore', emoji: '🇸🇬' },
{ value: 'SI', label: 'Slovenia', emoji: '🇸🇮' },
{ value: 'SK', label: 'Slovakia', emoji: '🇸🇰' },
{ value: 'SL', label: 'Sierra Leone', emoji: '🇸🇱' },
{ value: 'SM', label: 'San Marino', emoji: '🇸🇲' },
{ value: 'SN', label: 'Senegal', emoji: '🇸🇳' },
{ value: 'SO', label: 'Somalia', emoji: '🇸🇴' },
{ value: 'SR', label: 'Suriname', emoji: '🇸🇷' },
{ value: 'SS', label: 'South Sudan', emoji: '🇸🇸' },
{ value: 'ST', label: 'Sao Tome and Principe', emoji: '🇸🇹' },
{ value: 'SV', label: 'El Salvador', emoji: '🇸🇻' },
{ value: 'SY', label: 'Syria', emoji: '🇸🇾' },
{ value: 'SZ', label: 'Eswatini', emoji: '🇸🇿' },
{ value: 'TC', label: 'Turks and Caicos Islands', emoji: '🇹🇨' },
{ value: 'TD', label: 'Chad', emoji: '🇹🇩' },
{ value: 'TG', label: 'Togo', emoji: '🇹🇬' },
{ value: 'TH', label: 'Thailand', emoji: '🇹🇭' },
{ value: 'TJ', label: 'Tajikistan', emoji: '🇹🇯' },
{ value: 'TK', label: 'Tokelau', emoji: '🇹🇰' },
{ value: 'TL', label: 'Timor-Leste', emoji: '🇹🇱' },
{ value: 'TM', label: 'Turkmenistan', emoji: '🇹🇲' },
{ value: 'TN', label: 'Tunisia', emoji: '🇹🇳' },
{ value: 'TO', label: 'Tonga', emoji: '🇹🇴' },
{ value: 'TR', label: 'Türkiye', emoji: '🇹🇷' },
{ value: 'TT', label: 'Trinidad and Tobago', emoji: '🇹🇹' },
{ value: 'TV', label: 'Tuvalu', emoji: '🇹🇻' },
{ value: 'TW', label: 'Taiwan', emoji: '🇹🇼' },
{ value: 'TZ', label: 'Tanzania', emoji: '🇹🇿' },
{ value: 'UA', label: 'Ukraine', emoji: '🇺🇦' },
{ value: 'UG', label: 'Uganda', emoji: '🇺🇬' },
{ value: 'US', label: 'United States', emoji: '🇺🇸' },
{ value: 'UY', label: 'Uruguay', emoji: '🇺🇾' },
{ value: 'UZ', label: 'Uzbekistan', emoji: '🇺🇿' },
{ value: 'VA', label: 'Vatican City', emoji: '🇻🇦' },
{ value: 'VC', label: 'Saint Vincent and the Grenadines', emoji: '🇻🇨' },
{ value: 'VE', label: 'Venezuela', emoji: '🇻🇪' },
{ value: 'VG', label: 'British Virgin Islands', emoji: '🇻🇬' },
{ value: 'VI', label: 'U.S. Virgin Islands', emoji: '🇻🇮' },
{ value: 'VN', label: 'Vietnam', emoji: '🇻🇳' },
{ value: 'VU', label: 'Vanuatu', emoji: '🇻🇺' },
{ value: 'WF', label: 'Wallis and Futuna', emoji: '🇼🇫' },
{ value: 'WS', label: 'Samoa', emoji: '🇼🇸' },
{ value: 'YE', label: 'Yemen', emoji: '🇾🇪' },
{ value: 'YT', label: 'Mayotte', emoji: '🇾🇹' },
{ value: 'ZA', label: 'South Africa', emoji: '🇿🇦' },
{ value: 'ZM', label: 'Zambia', emoji: '🇿🇲' },
{ value: 'ZW', label: 'Zimbabwe', emoji: '🇿🇼' },
]
Example not foundExample not foundExample not foundGuides
Custom Router Links
Customize the navigate prop on Combobox.Root to integrate with your router. Using 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>
)
}
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,
})
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
Props
Root
| Prop | Default | Type |
|---|---|---|
collection | ListCollection<T>The collection of items | |
allowCustomValue | booleanWhether to allow typing custom values in the input | |
alwaysSubmitOnEnter | false | booleanWhether to always submit on Enter key press, even if popup is open. Useful for single-field autocomplete forms where Enter should submit the form. |
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' | 'clear' | 'replace' | '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-required] | Present when required |
[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 |
|---|---|
--reference-width | The width of the reference element |
--reference-height | The height of the root |
--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 |
--transform-origin | The transform origin for animations |
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 |
Context
These are the properties available when using Combobox.Context, useComboboxContext hook or useCombobox hook.
API
| Property | Type |
|---|---|
focused | booleanWhether the combobox is focused |
open | booleanWhether the combobox is open |
inputValue | stringThe value of the combobox input |
highlightedValue | stringThe value of the highlighted item |
highlightedItem | VThe highlighted item |
setHighlightValue | (value: string) => voidThe value of the combobox input |
clearHighlightValue | VoidFunctionFunction to clear the highlighted value |
syncSelectedItems | VoidFunctionFunction to sync the selected items with the value. Useful when `value` is updated from async sources. |
selectedItems | V[]The selected items |
hasSelectedItems | booleanWhether there's a selected item |
value | string[]The selected item keys |
valueAsString | stringThe string representation of the selected items |
selectValue | (value: string) => voidFunction to select a value |
setValue | (value: string[]) => voidFunction to set the value of the combobox |
clearValue | (value?: string) => voidFunction to clear the value of the combobox |
focus | VoidFunctionFunction to focus on the combobox input |
setInputValue | (value: string, reason?: InputValueChangeReason) => voidFunction to set the input value of the combobox |
getItemState | (props: ItemProps) => ItemStateReturns the state of a combobox item |
setOpen | (open: boolean, reason?: OpenChangeReason) => voidFunction to open or close the combobox |
collection | ListCollection<V>Function to toggle the combobox |
reposition | (options?: Partial<PositioningOptions>) => voidFunction to set the positioning options |
multiple | booleanWhether the combobox allows multiple selections |
disabled | booleanWhether the combobox is 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 |