Combobox
A combobox is an input widget with an associated popup that enables users to select a value from a collection of possible values.
- Zambia
- Benin
- Canada
- United States
- Japan
- Nigeria
- Albania
- Algeria
- American Samoa
- Andorra
- Angola
- Anguilla
- Antarctica
- Australia
- Austria
- Azerbaijan
- Bahamas
- Bahrain
- Madagascar
- Malawi
- Malaysia
- Maldives
- Mali
- Malta
- Togo
- Tokelau
- Tonga
- Trinidad and Tobago
- Tunisia
Features
- Support for selecting multiple values
- Support for disabled options
- Support for custom user input values
- Support for mouse, touch, and keyboard interactions
- Keyboard support for opening the combo box list box using the arrow keys, including automatically focusing the first or last item accordingly
Installation
To use the combobox machine in your project, run the following command in your command line:
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
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.
Usage
First, import the combobox package into your project
import * as combobox from "@zag-js/combobox"
The combobox package exports these functions:
- machine— The state machine logic for the combobox widget.
- connect— The function that translates the machine's state to JSX attributes and event handlers.
- collection- The function that creates a collection interface from an array of items.
Next, import the required hooks and functions for your framework and use the combobox machine in your project 🔥
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
To set the initial value of the combobox, pass the defaultValue property to
the machine's context.
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
To control the value programmatically, pass the value and onValueChange
properties to the machine function.
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>
Selecting multiple values
To allow selecting multiple values, set the multiple property in the machine's
context to true.
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, the selected values of a combobox are displayed in the input element, when selecting multiple items, it is a better UX to render the selected value outside the combobox.
To achieve this you need to:
- 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
To make a combobox disabled, set the context's disabled property to true
const service = useMachine(combobox.machine, { disabled: true, })
Disabling an option
To make a combobox option disabled, pass the isItemDisabled property to the
collection function.
const service = useMachine(combobox.machine, { id: useId(), collection: combobox.collection({ items: countries, isItemDisabled(item) { return item.disabled }, }), })
Close on select
This behaviour ensures that the menu is closed when an option is selected and is
true by default. It's only concerned with when an option is selected with
pointer or enter key. To disable the behaviour, set the closeOnSelect property
in the machine's context to false.
const service = useMachine(combobox.machine, { closeOnSelect: false, })
Making the combobox readonly
To make a combobox readonly, set the context's readOnly property to true
const service = useMachine(combobox.machine, { readOnly: true, })
Listening for highlight changes
When an option is highlighted with the pointer or keyboard, use the
onHighlightChange property to listen for this change and do something with it.
const service = useMachine(combobox.machine, { id: useId(), onHighlightChange(details) { // details => { value: string | null; item: CollectionItem | null } console.log(details) }, })
Listening for value changes
When an item is selected, use onValueChange property to listen for this change
and do something with it.
const service = useMachine(combobox.machine, { onValueChange(details) { // details => { value: string[]; items: CollectionItem[] } console.log(details) }, })
Usage within forms
The combobox works when placed within a form and the form is submitted. We achieve this by:
- ensuring we emit the input event as the value changes.
- adding a nameattribute to the input so the value can be accessed in theFormData.
To get this feature working you need to pass a name option to the context.
const service = useMachine(combobox.machine, { name: "countries", })
Allowing custom values
By default, the combobox only allows selecting values from the collection. To
allow custom values, set the allowCustomValue property in the machine's
context to true.
const service = useMachine(combobox.machine, { allowCustomValue: true, })
Styling guide
Earlier, we mentioned that each combobox part has a data-part attribute added
to them to select and style them in the DOM.
Open and closed state
When the combobox is open or closed, the data-state attribute is added to the
content,control, input and control parts.
[data-part="control"][data-state="open|closed"] { /* styles for control open or state */ } [data-part="input"][data-state="open|closed"] { /* styles for control open or state */ } [data-part="trigger"][data-state="open|closed"] { /* styles for control open or state */ } [data-part="content"][data-state="open|closed"] { /* styles for control open or 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-selected 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:
- open- booleanThe controlled open state of the combobox
- defaultOpen- booleanThe initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox.
- ids- Partial<{ 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.
- inputValue- stringThe controlled value of the combobox's input
- 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.
- name- stringThe `name` attribute of the combobox's input. Useful for form submission
- form- stringThe associate form of the combobox.
- disabled- booleanWhether the combobox is disabled
- readOnly- booleanWhether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it
- invalid- booleanWhether the combobox is invalid
- required- booleanWhether the combobox is required
- placeholder- stringThe placeholder text of the combobox's input
- defaultHighlightedValue- stringThe initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox.
- highlightedValue- stringThe controlled highlighted value of the combobox
- value- string[]The controlled value of the combobox's selected items
- 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.
- 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 updated
- selectionBehavior- "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
- autoFocus- booleanWhether to autofocus the input on mount
- openOnClick- booleanWhether to open the combobox popup on initial click on the input
- openOnChange- boolean | ((details: InputValueChangeDetails) => boolean)Whether to show the combobox when the input value changes
- allowCustomValue- booleanWhether to allow typing custom values in the input
- loopFocus- booleanWhether to loop the keyboard navigation through the items
- positioning- PositioningOptionsThe positioning options to dynamically position the menu
- onInputValueChange- (details: InputValueChangeDetails) => voidFunction called when the input's value changes
- onValueChange- (details: ValueChangeDetails<T>) => voidFunction called when a new item is selected
- onHighlightChange- (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 selected
- onOpenChange- (details: OpenChangeDetails) => voidFunction called when the popup is opened
- translations- IntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their states
- collection- ListCollection<T>The collection of 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.
- closeOnSelect- booleanWhether to close the combobox when an item is selected.
- openOnKeyPress- booleanWhether to open the combobox on arrow key press
- scrollToIndexFn- (details: ScrollToIndexDetails) => voidFunction to scroll to a specific index
- composite- booleanWhether the combobox is a composed with other composite widgets like tabs
- disableLayer- booleanWhether to disable registering this a dismissable layer
- navigate- (details: NavigateDetails) => voidFunction to navigate to the selected item
- dir- "ltr" | "rtl"The document's text/writing direction.
- id- stringThe 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 component
- onFocusOutside- (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component
- onInteractOutside- (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component
Machine API
The combobox api exposes the following methods:
- 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
- syncSelectedItems- () => voidFunction 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- () => voidFunction to focus on the combobox input
- setInputValue- (value: string) => voidFunction to set the input value of the combobox
- getItemState- (props: ItemProps) => ItemStateReturns the state of a combobox item
- setOpen- (open: boolean) => 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
Data Attributes
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
Edit this page on GitHub