Combobox
A combobox is an input widget with an associated popup that enables users to select a value from a collection of possible values.
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/vue # or yarn add @zag-js/combobox @zag-js/vue
npm install @zag-js/combobox @zag-js/vue # or yarn add @zag-js/combobox @zag-js/vue
npm install @zag-js/combobox @zag-js/solid # or yarn add @zag-js/combobox @zag-js/solid
This command will install the framework agnostic combobox logic and the reactive utilities for your framework of choice.
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-part
attribute 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" const comboboxData = [ { label: "Zambia", code: "ZA" }, { label: "Benin", code: "BN" }, //... ] export function Combobox() { const [options, setOptions] = useState(comboboxData) const collection = combobox.collection({ items: comboboxData, itemToValue: (item) => item.code, itemToString: (item) => item.label, }) const [state, send] = useMachine( combobox.machine({ id: useId(), collection, onOpenChange(details) { if (!details.open) return setOptions(comboboxData) }, onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) setOptions(filtered.length > 0 ? filtered : comboboxData) }, }), { context: { collection }, }, ) const api = combobox.connect(state, send, normalizeProps) return ( <div> <div {...api.rootProps}> <label {...api.labelProps}>Select country</label> <div {...api.controlProps}> <input {...api.inputProps} /> <button {...api.triggerProps}>▼</button> </div> </div> <div {...api.positionerProps}> {options.length > 0 && ( <ul {...api.contentProps}> {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/vue" import { computed, defineComponent, ref } from "vue" const comboboxData = [ { label: "Zambia", code: "ZA" }, { label: "Benin", code: "BN" }, //... ] export default defineComponent({ name: "Combobox", setup() { const options = ref(comboboxData) const collectionRef = computed(() => combobox.collection({ items: options.value, itemToString: (item) => item.label, itemToValue: (item) => item.code, }), ) const [state, send] = useMachine( combobox.machine({ id: "v1", collection: collectionRef.value, onOpenChange(details) { if (!details.open) return options.value = comboboxData }, onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) options.value = filtered.length > 0 ? filtered : comboboxData }, }), { context: computed(() => ({ collection: collectionRef.value, })), }, ) const apiRef = computed(() => combobox.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div> <div {...api.rootProps}> <label {...api.labelProps}>Select country</label> <div {...api.controlProps}> <input {...api.inputProps} /> <button {...api.triggerProps}>▼</button> </div> </div> <div {...api.positionerProps}> {options.value.length > 0 && ( <ul {...api.contentProps}> {options.value.map((item) => ( <li key={item.code} {...api.getItemProps({ item })}> {item.label} </li> ))} </ul> )} </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 [state, send] = useMachine( combobox.machine({ id: "1", collection: collectionRef.value, onOpenChange(details) { if (!details.open) return options.value = comboboxData }, onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) options.value = filtered.length > 0 ? filtered : comboboxData }, }), { context: computed(() => ({ collection: collectionRef.value, })), }, ) const api = computed(() => combobox.connect(state.value, send, normalizeProps), ) </script> <template> <div v-bind="api.rootProps"> <label v-bind="api.labelProps">Select country</label> <div v-bind="api.controlProps"> <input v-bind="api.inputProps" /> <button v-bind="api.triggerProps">▼</button> </div> </div> <div v-bind="api.positionerProps"> <ul v-if="options.length > 0" v-bind="api.contentProps"> <li v-for="item in options" :key="item.code" v-bind="api.getItemProps({ item })" > {{ item.label }} </li> </ul> </div> </template>
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 [state, send] = useMachine( combobox.machine({ id: createUniqueId(), collection: collection(), onOpenChange(details) { if (!details.open) return setOptions(comboboxData) }, onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) setOptions(filtered.length > 0 ? filtered : comboboxData) }, }), ) const api = createMemo(() => combobox.connect(state, send, normalizeProps)) return ( <div> <div {...api().rootProps}> <label {...api().labelProps}>Select country</label> <div {...api().controlProps}> <input {...api().inputProps} /> <button {...api().triggerProps}>▼</button> </div> </div> <div {...api().positionerProps}> <Show when={options().length > 0}> <ul {...api().contentProps}> <For each={options()}> {(item) => ( <li {...api().getItemProps({ item })}>{item.label}</li> )} </For> </ul> </Show> </div> </div> ) }
Setting the initial value
To set the initial value of the combobox, pass the value
property to the
machine's context.
The
value
property must be an array of strings. If selecting a single value, pass an array with a single string.
const collection = combobox.collection({ items: [ { label: "Nigeria", value: "ng" }, { label: "Ghana", value: "gh" }, { label: "Kenya", value: "ke" }, //... ], }) const [state, send] = useMachine( combobox.machine({ id: useId(), collection, value: ["ng"], }), )
Selecting multiple values
To allow selecting multiple values, set the multiple
property in the machine's
context to true
.
const [state, send] = useMachine( combobox.machine({ id: useId(), collection, multiple: true, }), )
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
selectionBehavior
toclear
, which clears the input value when an item is selected. - Set the
multiple
property totrue
to allow selecting multiple values. - Render the selected values outside the combobox.
const [state, send] = 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 [state, send] = useMachine( combobox.machine({ disabled: true, }), )
Disabling an option
To make a combobox option disabled, pass the isItemDisabled
property to the
collection function.
const [state, send] = 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 [state, send] = useMachine( combobox.machine({ closeOnSelect: false, }), )
Making the combobox readonly
To make a combobox readonly, set the context's readOnly
property to true
const [state, send] = 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 [state, send] = 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 [state, send] = 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
name
attribute 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 [state, send] = 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 [state, send] = 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:
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
string
The current value of the combobox's inputname
string
The `name` attribute of the combobox's input. Useful for form submissionform
string
The associate form of the combobox.disabled
boolean
Whether the combobox is disabledreadOnly
boolean
Whether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with itinvalid
boolean
Whether the combobox is requiredplaceholder
string
The placeholder text of the combobox's inputhighlightedValue
string
The active item's id. Used to set the `aria-activedescendant` attributevalue
string[]
The keys of the selected itemsinputBehavior
"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 preservedselectOnBlur
boolean
Whether to select the higlighted item on interaction outside the comboboxautoFocus
boolean
Whether to autofocus the input on mountopenOnClick
boolean
Whether to open the combobox popup on initial click on the inputallowCustomValue
boolean
Whether to allow custom values or free values in the inputloop
boolean
Whether to loop the keyboard navigation through the itemspositioning
PositioningOptions
The positioning options to dynamically position the menuonInputValueChange
(details: InputValueChangeDetails) => void
Function called when the input's value changesonValueChange
(details: ValueChangeDetails<T>) => void
Function called when a new item is selectedonHighlightChange
(details: HighlightChangeDetails<T>) => void
Function called when an item is highlighted using the pointer or keyboard navigation.onOpenChange
(details: OpenChangeDetails) => void
Function called when the popup is openedtranslations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their statescollection
Collection<any>
The collection of itemsmultiple
boolean
Whether to allow multiple selectioncloseOnSelect
boolean
Whether to close the combobox when an item is selected.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the comboboxonFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the comboboxonInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the combobox
Machine API
The combobox api
exposes the following methods:
isFocused
boolean
Whether the combobox is focusedisOpen
boolean
Whether the combobox is openisInputValueEmpty
boolean
Whether the combobox input value is emptyinputValue
string
The value of the combobox inputhighlightedValue
string
The value of the highlighted itemhighlightedItem
V
The highlighted itemhighlightValue
(value: string) => void
The value of the combobox inputselectedItems
V[]
The selected itemshasSelectedItems
boolean
Whether there's a selected itemvalue
string[]
The selected item keysvalueAsString
string
The string representation of the selected itemsselectValue
(value: string) => void
Function to select a valuesetValue
(value: string[]) => void
Function to set the value of the comboboxclearValue
(value?: string) => void
Function to clear the value of the comboboxfocus
() => void
Function to focus on the combobox inputsetInputValue
(value: string) => void
Function to set the input value of the comboboxgetItemState
(props: ItemProps) => ItemState
Returns the state of a combobox itemopen
() => void
Function to open the comboboxclose
() => void
Function to close the comboboxsetCollection
(collection: Collection<V>) => void
Function to set the collection of itemsreposition
(options: Partial<PositioningOptions>) => void
Function to set the positioning options
Edit this page on GitHub