Combobox
A combobox is an input with a popup that lets you select a value from a collection.
Features
- Supports selecting multiple values
- Supports disabled options
- Supports custom user input values
- Supports mouse, touch, and keyboard interactions
- Supports opening the combobox listbox with arrow keys, including automatically focusing the first or last item accordingly
Installation
Install the combobox package:
npm install @zag-js/combobox @zag-js/react # or yarn add @zag-js/combobox @zag-js/react
npm install @zag-js/combobox @zag-js/solid # or yarn add @zag-js/combobox @zag-js/solid
npm install @zag-js/combobox @zag-js/vue # or yarn add @zag-js/combobox @zag-js/vue
npm install @zag-js/combobox @zag-js/svelte # or yarn add @zag-js/combobox @zag-js/svelte
Anatomy
Check the combobox anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the combobox package:
import * as combobox from "@zag-js/combobox"
These are the key exports:
machine- Behavior logic.connect- Maps behavior to JSX props and event handlers.collection- Creates a collection interface from an array of items.
Then use the framework integration helpers:
import * as combobox from "@zag-js/combobox" import { useMachine, normalizeProps } from "@zag-js/react" import { useState, useId } from "react" const comboboxData = [ { label: "Zambia", code: "ZA" }, { label: "Benin", code: "BN" }, //... ] export function Combobox() { const [options, setOptions] = useState(comboboxData) const collection = combobox.collection({ items: options, itemToValue: (item) => item.code, itemToString: (item) => item.label, }) const service = useMachine(combobox.machine, { id: useId(), collection, onOpenChange() { setOptions(comboboxData) }, onInputValueChange({ inputValue }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(inputValue.toLowerCase()), ) setOptions(filtered.length > 0 ? filtered : comboboxData) }, }) const api = combobox.connect(service, normalizeProps) return ( <div> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Select country</label> <div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getTriggerProps()}>▼</button> </div> </div> <div {...api.getPositionerProps()}> {options.length > 0 && ( <ul {...api.getContentProps()}> {options.map((item) => ( <li key={item.code} {...api.getItemProps({ item })}> {item.label} </li> ))} </ul> )} </div> </div> ) }
import * as combobox from "@zag-js/combobox" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createSignal, createUniqueId, For, Show } from "solid-js" const comboboxData = [ { label: "Zambia", code: "ZA" }, { label: "Benin", code: "BN" }, //... ] export function Combobox() { const [options, setOptions] = createSignal(comboboxData) const collection = createMemo(() => combobox.collection({ items: options(), itemToValue: (item) => item.code, itemToString: (item) => item.label, }), ) const service = useMachine(combobox.machine, { id: createUniqueId(), get collection() { return collection() }, onOpenChange() { setOptions(comboboxData) }, onInputValueChange({ inputValue }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(inputValue.toLowerCase()), ) setOptions(filtered.length > 0 ? filtered : comboboxData) }, }) const api = createMemo(() => combobox.connect(service, normalizeProps)) return ( <div> <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Select country</label> <div {...api().getControlProps()}> <input {...api().getInputProps()} /> <button {...api().getTriggerProps()}>▼</button> </div> </div> <div {...api().getPositionerProps()}> <Show when={options().length > 0}> <ul {...api().getContentProps()}> <For each={options()}> {(item) => ( <li {...api().getItemProps({ item })}>{item.label}</li> )} </For> </ul> </Show> </div> </div> ) }
<script setup> import * as combobox from "@zag-js/combobox" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, ref } from "vue" const comboboxData = [ { label: "Zambia", code: "ZA" }, { label: "Benin", code: "BN" }, //... ] const options = ref(comboboxData) const collectionRef = computed(() => combobox.collection({ items: options.value, itemToValue: (item) => item.code, itemToString: (item) => item.label, }), ) const service = useMachine(combobox.machine, { id: "1", get collection() { return collectionRef.value }, onOpenChange() { options.value = comboboxData }, onInputValueChange({ inputValue }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(inputValue.toLowerCase()), ) options.value = filtered.length > 0 ? filtered : comboboxData }, }) const api = computed(() => combobox.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Select country</label> <div v-bind="api.getControlProps()"> <input v-bind="api.getInputProps()" /> <button v-bind="api.getTriggerProps()">▼</button> </div> </div> <div v-bind="api.getPositionerProps()"> <ul v-if="options.length > 0" v-bind="api.getContentProps()"> <li v-for="item in options" :key="item.code" v-bind="api.getItemProps({ item })" > {{ item.label }} </li> </ul> </div> </template>
<script lang="ts"> import * as combobox from "@zag-js/combobox" import { useMachine, normalizeProps } from "@zag-js/svelte" const comboboxData = [ { label: "Zambia", code: "ZA" }, { label: "Benin", code: "BN" }, //... ] let options = $state.raw(comboboxData) const collection = $derived(combobox.collection({ items: options, itemToValue: (item) => item.code, itemToString: (item) => item.label, })) const id = $props.id() const service = useMachine(combobox.machine, { id, get collection() { return collection }, onOpenChange() { options = comboboxData }, onInputValueChange({ inputValue }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(inputValue.toLowerCase()), ) const newOptions = filtered.length > 0 ? filtered : comboboxData options = newOptions }, }) const api = $derived(combobox.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Select country</label> <div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getTriggerProps()}>▼</button> </div> </div> <div {...api.getPositionerProps()}> {#if options.length > 0} <ul {...api.getContentProps()}> {#each options as item} <li {...api.getItemProps({ item })}>{item.label}</li> {/each} </ul> {/if} </div>
Setting the initial value
Set defaultValue to define the initial combobox value.
const collection = combobox.collection({ items: [ { label: "Nigeria", value: "ng" }, { label: "Ghana", value: "gh" }, { label: "Kenya", value: "ke" }, //... ], }) const service = useMachine(combobox.machine, { id: useId(), collection, defaultValue: ["ng"], })
Controlled combobox
Use value and onValueChange to control the value programmatically.
import { useState } from "react" export function ControlledCombobox() { const [value, setValue] = useState(["ng"]) const service = useMachine(combobox.machine, { value, onValueChange(details) { setValue(details.value) } }) return ( // ... ) }
import { createSignal } from "solid-js" export function ControlledCombobox() { const [value, setValue] = createSignal(["ng"]) const service = useMachine(combobox.machine, { get value() { return value() }, onValueChange(details) { setValue(details.value) } }) return ( // ... ) }
<script setup lang="ts"> import { ref } from "vue" const valueRef = ref(["ng"]) const service = useMachine(combobox.machine, { get value() { return valueRef.value }, onValueChange(details) { valueRef.value = details.value }, }) </script>
<script lang="ts"> let value = $state(["ng"]) const service = useMachine(combobox.machine, { get value() { return value }, onValueChange(details) { value = details.value }, }) </script>
Setting the initial input value
Use defaultInputValue to prefill the input on first render.
const service = useMachine(combobox.machine, { id: useId(), collection, defaultInputValue: "Nig", })
Controlling the input value
Use inputValue and onInputValueChange when you want to filter options as you
type.
const service = useMachine(combobox.machine, { id: useId(), collection, inputValue, onInputValueChange({ inputValue }) { setInputValue(inputValue) setOptions(filterItems(inputValue)) }, })
Selecting multiple values
Set multiple to true to allow selecting multiple values.
const service = useMachine(combobox.machine, { id: useId(), collection, multiple: true, })
Using a custom object format
By default, the combobox collection expects an array of items with label and
value properties. To use a custom object format, pass the itemToString and
itemToValue properties to the collection function.
itemToString— A function that returns the string representation of an item. Used to compare items when filtering.itemToValue— A function that returns the unique value of an item.itemToDisabled— A function that returns the disabled state of an item.
const collection = combobox.collection({ // custom object format items: [ { id: 1, fruit: "Banana", available: true, quantity: 10 }, { id: 2, fruit: "Apple", available: false, quantity: 5 }, { id: 3, fruit: "Orange", available: true, quantity: 3 }, //... ], // convert item to string itemToString(item) { return item.fruit }, // convert item to value itemToValue(item) { return item.id }, // convert item to disabled state itemToDisabled(item) { return !item.available || item.quantity === 0 }, }) // use the collection const service = useMachine(combobox.machine, { id: useId(), collection, })
Rendering the selected values outside the combobox
By default, selected values are shown in the input. For multiple selection, it is often better to render selected values outside the combobox.
To do that:
- Set the
selectionBehaviortoclear, which clears the input value when an item is selected. - Set the
multipleproperty totrueto allow selecting multiple values. - Render the selected values outside the combobox.
const service = useMachine(combobox.machine, { id: useId(), collection, selectionBehavior: "clear", multiple: true, })
Disabling the combobox
Set disabled to true to disable the combobox.
const service = useMachine(combobox.machine, { disabled: true, })
Disabling an option
Pass isItemDisabled to disable specific options.
const service = useMachine(combobox.machine, { id: useId(), collection: combobox.collection({ items: countries, isItemDisabled(item) { return item.disabled }, }), })
Close on select
By default, the menu closes when an option is selected with pointer or enter
key. Set closeOnSelect to false to keep it open.
const service = useMachine(combobox.machine, { closeOnSelect: false, })
Controlling open state
Use open and onOpenChange for controlled popup state, or defaultOpen for
an 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, { id: useId(), collection, defaultOpen: true, })
Configuring popup trigger behavior
Use these props to fine-tune when the popup opens:
openOnClickto open when the input is clickedopenOnChangeto control opening on input changesopenOnKeyPressto control opening on arrow key pressinputBehaviorto set how typing and keyboard navigation affect highlight/input
const service = useMachine(combobox.machine, { id: useId(), collection, openOnClick: true, openOnChange: false, openOnKeyPress: false, inputBehavior: "autohighlight", })
Positioning the popup
Use positioning to control how the popup is placed.
const service = useMachine(combobox.machine, { id: useId(), collection, positioning: { placement: "bottom-start" }, })
Submitting forms on Enter
Set alwaysSubmitOnEnter to true if you want Enter to submit the form even
while the popup is open.
const service = useMachine(combobox.machine, { id: useId(), collection, alwaysSubmitOnEnter: true, })
Making the combobox readonly
Set readOnly to true to make the combobox read-only.
const service = useMachine(combobox.machine, { readOnly: true, })
Required and invalid state
Set required and invalid for form validation and UI state.
const service = useMachine(combobox.machine, { id: useId(), collection, required: true, invalid: false, })
Listening for highlight changes
Use onHighlightChange to listen for highlighted option changes.
const service = useMachine(combobox.machine, { id: useId(), onHighlightChange(details) { // details => { highlightedValue: string | null; highlightedItem: CollectionItem | null } console.log(details) }, })
Setting the initial highlighted option
Use defaultHighlightedValue to set which option is highlighted when the popup
opens.
const service = useMachine(combobox.machine, { id: useId(), collection, defaultHighlightedValue: "ng", })
Listening for item selection
Use onSelect to react to each selected item.
const service = useMachine(combobox.machine, { id: useId(), collection, onSelect(details) { // details => { value: string[]; itemValue: string } console.log(details.itemValue) }, })
Listening for value changes
Use onValueChange to listen for selected value changes.
const service = useMachine(combobox.machine, { onValueChange(details) { // details => { value: string[]; items: CollectionItem[] } console.log(details) }, })
Usage within forms
The combobox works in forms when you:
- add a
nameso the selected value is included inFormData.
Set name to enable form submission support.
const service = useMachine(combobox.machine, { name: "countries", })
If the input belongs to a different form element, also set form.
const service = useMachine(combobox.machine, { id: useId(), collection, name: "countries", form: "checkout-form", })
Allowing custom values
By default, the combobox only allows values from the collection. Set
allowCustomValue to true to allow custom values.
const service = useMachine(combobox.machine, { allowCustomValue: true, })
Customizing accessibility labels
Use translations to customize the trigger and clear button labels.
const service = useMachine(combobox.machine, { id: useId(), collection, translations: { triggerLabel: "Open countries", clearTriggerLabel: "Clear selection", }, })
Styling guide
Each combobox part includes a data-part attribute you can target in CSS.
Open and closed state
When the combobox opens or closes, data-state is added to content, control,
input, and trigger parts.
[data-part="control"][data-state="open|closed"] { /* styles for open or closed control state */ } [data-part="input"][data-state="open|closed"] { /* styles for open or closed input state */ } [data-part="trigger"][data-state="open|closed"] { /* styles for open or closed trigger state */ } [data-part="content"][data-state="open|closed"] { /* styles for open or closed content state */ }
Focused State
When the combobox is focused, the data-focus attribute is added to the control
and label parts.
[data-part="control"][data-focus] { /* styles for control focus state */ } [data-part="label"][data-focus] { /* styles for label focus state */ }
Disabled State
When the combobox is disabled, the data-disabled attribute is added to the
label, control, trigger and option parts.
[data-part="label"][data-disabled] { /* styles for label disabled state */ } [data-part="control"][data-disabled] { /* styles for control disabled state */ } [data-part="trigger"][data-disabled] { /* styles for trigger disabled state */ } [data-part="item"][data-disabled] { /* styles for item disabled state */ }
Invalid State
When the combobox is invalid, the data-invalid attribute is added to the root,
label, control and input parts.
[data-part="root"][data-invalid] { /* styles for root invalid state */ } [data-part="label"][data-invalid] { /* styles for label invalid state */ } [data-part="control"][data-invalid] { /* styles for control invalid state */ } [data-part="input"][data-invalid] { /* styles for input invalid state */ }
Selected State
When a combobox item is selected, the data-state attribute is added to the
item part.
[data-part="item"][data-state="checked|unchecked"] { /* styles for item selected state */ }
Highlighted State
When a combobox item is highlighted, the data-highlighted attribute is added
to the item part.
[data-part="item"][data-highlighted] { /* styles for item highlighted state */ }
Methods and Properties
Machine Context
The combobox machine exposes the following context properties:
openbooleanThe controlled open state of the comboboxdefaultOpenbooleanThe initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox.idsPartial<{ root: string; label: string; control: string; input: string; content: string; trigger: string; clearTrigger: string; item: (id: string, index?: number) => string; positioner: string; itemGroup: (id: string | number) => string; itemGroupLabel: (id: string | number) => string; }>The ids of the elements in the combobox. Useful for composition.inputValuestringThe controlled value of the combobox's inputdefaultInputValuestringThe initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input.namestringThe `name` attribute of the combobox's input. Useful for form submissionformstringThe associate form of the combobox.disabledbooleanWhether the combobox is disabledreadOnlybooleanWhether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with itinvalidbooleanWhether the combobox is invalidrequiredbooleanWhether the combobox is requiredplaceholderstringThe placeholder text of the combobox's inputdefaultHighlightedValuestringThe initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox.highlightedValuestringThe controlled highlighted value of the comboboxvaluestring[]The controlled value of the combobox's selected itemsdefaultValuestring[]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.inputBehavior"autohighlight" | "autocomplete" | "none"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 updatedselectionBehavior"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 preservedautoFocusbooleanWhether to autofocus the input on mountopenOnClickbooleanWhether to open the combobox popup on initial click on the inputopenOnChangeboolean | ((details: InputValueChangeDetails) => boolean)Whether to show the combobox when the input value changesallowCustomValuebooleanWhether to allow typing custom values in the inputalwaysSubmitOnEnterbooleanWhether to always submit on Enter key press, even if popup is open. Useful for single-field autocomplete forms where Enter should submit the form.loopFocusbooleanWhether to loop the keyboard navigation through the itemspositioningPositioningOptionsThe positioning options to dynamically position the menuonInputValueChange(details: InputValueChangeDetails) => voidFunction called when the input's value changesonValueChange(details: ValueChangeDetails<T>) => voidFunction called when a new item is selectedonHighlightChange(details: HighlightChangeDetails<T>) => voidFunction called when an item is highlighted using the pointer or keyboard navigation.onSelect(details: SelectionDetails) => voidFunction called when an item is selectedonOpenChange(details: OpenChangeDetails) => voidFunction called when the popup is openedtranslationsIntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their statescollectionListCollection<T>The collection of itemsmultiplebooleanWhether 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.closeOnSelectbooleanWhether to close the combobox when an item is selected.openOnKeyPressbooleanWhether to open the combobox on arrow key pressscrollToIndexFn(details: ScrollToIndexDetails) => voidFunction to scroll to a specific indexcompositebooleanWhether the combobox is a composed with other composite widgets like tabsdisableLayerbooleanWhether to disable registering this a dismissable layernavigate(details: NavigateDetails) => voidFunction to navigate to the selected itemdir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the componentonFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the componentonInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component
Machine API
The combobox api exposes the following methods:
focusedbooleanWhether the combobox is focusedopenbooleanWhether the combobox is openinputValuestringThe value of the combobox inputhighlightedValuestringThe value of the highlighted itemhighlightedItemVThe highlighted itemsetHighlightValue(value: string) => voidThe value of the combobox inputclearHighlightValueVoidFunctionFunction to clear the highlighted valuesyncSelectedItemsVoidFunctionFunction to sync the selected items with the value. Useful when `value` is updated from async sources.selectedItemsV[]The selected itemshasSelectedItemsbooleanWhether there's a selected itemvaluestring[]The selected item keysvalueAsStringstringThe string representation of the selected itemsselectValue(value: string) => voidFunction to select a valuesetValue(value: string[]) => voidFunction to set the value of the comboboxclearValue(value?: string) => voidFunction to clear the value of the comboboxfocusVoidFunctionFunction to focus on the combobox inputsetInputValue(value: string, reason?: InputValueChangeReason) => voidFunction to set the input value of the comboboxgetItemState(props: ItemProps) => ItemStateReturns the state of a combobox itemsetOpen(open: boolean, reason?: OpenChangeReason) => voidFunction to open or close the comboboxcollectionListCollection<V>Function to toggle the comboboxreposition(options?: Partial<PositioningOptions>) => voidFunction to set the positioning optionsmultiplebooleanWhether the combobox allows multiple selectionsdisabledbooleanWhether the combobox is disabled
Data Attributes
CSS Variables
Accessibility
Adheres to the Combobox WAI-ARIA design pattern.
Keyboard Interactions
- ArrowDownWhen the combobox is closed, opens the listbox and highlights to the first option. When the combobox is open, moves focus to the next option.
- ArrowUpWhen the combobox is closed, opens the listbox and highlights to the last option. When the combobox is open, moves focus to the previous option.
- HomeWhen the combobox is open, moves focus to the first option.
- EndWhen the combobox is open, moves focus to the last option.
- EscapeCloses the listbox.
- EnterSelects the highlighted option and closes the combobox.
- EscCloses the combobox