A combobox combines a text input with a popup listbox, allowing users to either type to filter options or navigate with the keyboard. It follows the WAI-ARIA Combobox pattern.
Features
- Supports single and multiple selection
- Supports disabled options
- Supports custom user input values
- Mouse, touch, and keyboard interactions
- Opens listbox with arrow keys, auto-focusing the first or last item
- Controlled and uncontrolled modes
Installation
npm install @zag-js/combobox @zag-js/react
Import the combobox package and create a collection:
import * as combobox from "@zag-js/combobox"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"
The combobox package exports three key functions:
machine — Behavior logic.
connect — Maps behavior to JSX props and event handlers.
collection — Creates a list collection from an array of items.
import * as combobox from "@zag-js/combobox"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId, useState, useMemo } from "react"
const data = [
{ label: "Nigeria", value: "ng" },
{ label: "Ghana", value: "gh" },
{ label: "Kenya", value: "ke" },
{ label: "South Africa", value: "za" },
]
export function Combobox() {
const [inputValue, setInputValue] = useState("")
const items = useMemo(
() => data.filter((item) => item.label.toLowerCase().includes(inputValue.toLowerCase())),
[inputValue],
)
const collection = useMemo(
() => combobox.collection({ items }),
[items],
)
const service = useMachine(combobox.machine, {
id: useId(),
collection,
onInputValueChange({ inputValue }) {
setInputValue(inputValue)
},
})
const api = combobox.connect(service, normalizeProps)
return (
<div {...api.getRootProps()}>
<label {...api.getLabelProps()}>Country</label>
<div {...api.getControlProps()}>
<input {...api.getInputProps()} />
<button {...api.getTriggerProps()}>â–¼</button>
</div>
<div {...api.getPositionerProps()}>
<ul {...api.getContentProps()}>
{items.map((item) => (
<li key={item.value} {...api.getItemProps({ item })}>
{item.label}
</li>
))}
</ul>
</div>
</div>
)
}
<script setup>
import * as combobox from "@zag-js/combobox"
import { useMachine, normalizeProps } from "@zag-js/vue"
import { useId, computed, ref } from "vue"
const data = [
{ label: "Nigeria", value: "ng" },
{ label: "Ghana", value: "gh" },
{ label: "Kenya", value: "ke" },
]
const inputValue = ref("")
const items = computed(() =>
data.filter((item) => item.label.toLowerCase().includes(inputValue.value.toLowerCase()))
)
const collection = computed(() => combobox.collection({ items: items.value }))
const service = useMachine(combobox.machine, {
id: useId(),
get collection() { return collection.value },
onInputValueChange({ inputValue: v }) {
inputValue.value = v
},
})
const api = computed(() => combobox.connect(service, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()">
<label v-bind="api.getLabelProps()">Country</label>
<div v-bind="api.getControlProps()">
<input v-bind="api.getInputProps()" />
<button v-bind="api.getTriggerProps()">â–¼</button>
</div>
<div v-bind="api.getPositionerProps()">
<ul v-bind="api.getContentProps()">
<li
v-for="item in items"
:key="item.value"
v-bind="api.getItemProps({ item })"
>
{{ item.label }}
</li>
</ul>
</div>
</div>
</template>
Setting the initial value
Set defaultValue to define the initial selected value.
const service = useMachine(combobox.machine, {
id: useId(),
collection,
defaultValue: ["ng"],
})
Controlled combobox
Use value and onValueChange to control the selected value programmatically.
const [value, setValue] = useState(["ng"])
const service = useMachine(combobox.machine, {
id: useId(),
collection,
value,
onValueChange(details) {
// details => { value: string[], items: CollectionItem[] }
setValue(details.value)
},
})
Selecting multiple values
Set multiple to true to allow selecting multiple values.
const service = useMachine(combobox.machine, {
id: useId(),
collection,
multiple: true,
})
When multiple is true, selectionBehavior is automatically set to "clear". Render selected items outside the combobox for the best UX.
Using a custom object format
Pass itemToString, itemToValue, and optionally itemToDisabled to use a non-standard object shape.
const collection = combobox.collection({
items: [
{ id: 1, fruit: "Banana", available: true, quantity: 10 },
{ id: 2, fruit: "Apple", available: false, quantity: 5 },
],
itemToString(item) {
return item.fruit
},
itemToValue(item) {
return String(item.id)
},
itemToDisabled(item) {
return !item.available || item.quantity === 0
},
})
Allowing custom values
Set allowCustomValue to true to let users type a value not in the collection.
const service = useMachine(combobox.machine, {
allowCustomValue: true,
})
Controlling open state
Use open and onOpenChange for controlled popup state, or defaultOpen for uncontrolled initial state.
const service = useMachine(combobox.machine, {
id: useId(),
collection,
open,
onOpenChange(details) {
// details => { open: boolean; reason?: string; value: string[] }
setOpen(details.open)
},
})
const service = useMachine(combobox.machine, {
openOnClick: true, // open when input is clicked
openOnChange: false, // do not open on input changes
openOnKeyPress: false, // do not open on arrow key press
inputBehavior: "autohighlight", // highlight first item as user types
})
const service = useMachine(combobox.machine, {
positioning: { placement: "bottom-start" },
})
Usage within forms
Set name to include the selected value in form data.
const service = useMachine(combobox.machine, {
name: "countries",
form: "checkout-form", // optional: associate with a specific form
})
API Reference
collection
ListCollection<T>
required
The collection of items to display in the listbox.
The controlled selected value(s).
The initial selected value(s) when rendered.
The controlled value of the combobox input.
The initial value of the combobox input when rendered.
Whether to allow multiple selection.
Whether the combobox is disabled.
Whether the combobox is read-only.
Whether the combobox is in an invalid state.
Whether the combobox is required.
Placeholder text for the input.
Whether users can type a value not present in the collection.
Whether to close the popup when an item is selected. Defaults to true for single-select, false for multi-select.
selectionBehavior
"replace" | "clear" | "preserve"
default:"\"replace\""
What happens to the input value after selecting an item.
inputBehavior
"none" | "autohighlight" | "autocomplete"
default:"\"none\""
Auto-completion behavior while typing.
Options for dynamically positioning the popup.
onValueChange
(details: { value: string[], items: CollectionItem[] }) => void
Callback invoked when the selected value changes.
onInputValueChange
(details: { inputValue: string }) => void
Callback invoked when the input value changes.
onHighlightChange
(details: { highlightedValue: string | null }) => void
Callback invoked when the highlighted item changes.
onOpenChange
(details: { open: boolean }) => void
Callback invoked when the popup opens or closes.
Styling
Each combobox part includes a data-part attribute you can target in CSS.
| Part | Description |
|---|
root | Outermost wrapper |
label | The label element |
control | Wraps the input and trigger button |
input | The text input |
trigger | The dropdown toggle button |
clear-trigger | Button to clear the selection |
positioner | Positions the popup |
content | The popup listbox |
item | An individual list item |
item-text | The text inside a list item |
item-indicator | Selected indicator inside a list item |
Open and closed state
[data-part="control"][data-state="open"] {
/* styles for the open control */
}
[data-part="content"][data-state="closed"] {
/* styles for the closed content */
}
Focused state
[data-part="control"][data-focus] {
/* styles for control focus state */
}
[data-part="label"][data-focus] {
/* styles for label focus state */
}
Disabled state
[data-part="label"][data-disabled] { }
[data-part="control"][data-disabled] { }
[data-part="trigger"][data-disabled] { }
[data-part="item"][data-disabled] { }
Invalid state
[data-part="root"][data-invalid] { }
[data-part="input"][data-invalid] { }
Selected and highlighted items
[data-part="item"][data-state="checked"] {
/* styles for the selected item */
}
[data-part="item"][data-highlighted] {
/* styles for the highlighted item */
}
Accessibility
Adheres to the Combobox WAI-ARIA design pattern.
- The input has
role="combobox" with aria-expanded, aria-autocomplete, and aria-controls.
- The listbox has
role="listbox" and each option has role="option".
- Focus management uses
aria-activedescendant to indicate the highlighted option.
Keyboard interactions
| Key | Description |
|---|
ArrowDown | Opens the popup and moves highlight to the next item. |
ArrowUp | Opens the popup and moves highlight to the previous item. |
Enter | Selects the highlighted item and closes the popup. |
Escape | Closes the popup and clears the input (if allowCustomValue is false). |
Home | Moves highlight to the first item. |
End | Moves highlight to the last item. |
Alt + ArrowDown | Opens the popup without moving highlight. |
Alt + ArrowUp | Closes the popup if open. |