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 filtering a list of options by typing.
- 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.
On a high level, the combobox consists of:
- Root: The root container for the combobox
- Label: The label that gives the user information on the combobox
- Positioner: The element that positions the combobox dynamically.
- Content: The element that contains the options.
- Trigger: The element that toggles the combobox menu.
Usage
First, import the combobox package into your project
import * as combobox from "@zag-js/combobox"
The combobox package exports two key 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.
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 [state, send] = useMachine( combobox.machine({ id: useId(), onOpen() { setOptions(comboboxData) }, onInputChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) setOptions(filtered.length > 0 ? filtered : comboboxData) }, }), ) 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, index) => ( <li key={`${item.code}:${index}`} {...api.getOptionProps({ label: item.label, value: item.code, index, disabled: item.disabled, })} > {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 [state, send] = useMachine( combobox.machine({ id: "combobox", onOpen() { options.value = comboboxData }, onInputChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) options.value = filtered.length > 0 ? filtered : comboboxData }, }), ) 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, index) => ( <li key={`${item.code}:${index}`} {...api.getOptionProps({ label: item.label, value: item.code, index, disabled: item.disabled, })} > {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 [state, send] = useMachine( combobox.machine({ id: "combobox", onOpen() { options.value = comboboxData }, onInputChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) options.value = filtered.length > 0 ? filtered : comboboxData }, }), ) 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, index) in options" :key="item.code" v-bind="api.getOptionProps({ label: item.label, value: item.code, index, disabled: item.disabled, })" > {{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", disabled: false }, { label: "Benin", code: "BN", disabled: false }, //... ] export function Combobox() { const [options, setOptions] = createSignal(comboboxData) const [state, send] = useMachine( combobox.machine({ id: createUniqueId(), onOpen() { setOptions(comboboxData) }, onInputChange({ 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, index) => ( <li {...api().getOptionProps({ label: item.label, value: item.code, index: index(), disabled: item.disabled, })} > {item.label} </li> )} </For> </ul> </Show> </div> </div> ) }
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, set the disabled
property of
getOptionProps
to true
const [state, send] = useMachine(combobox.machine()) return { <li {...api.getOptionProps({ label: item.label, value: item.code, index:index, disabled: item.disabled, })} /> }
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, the
highlightedOption
property in the machine's context is updated. You can listen
for this change and do something with it.
const [state, send] = useMachine( combobox.machine({ id: useId(), onHighlight(details) { // details => { label: string, value: string, relatedTarget: HTMLElement | null } console.log(details) }, }), )
Listening for selection changes
When an option is selected, the selectedOption
property in the machine's
context is updated. You can listen for this change and do something with it.
const [state, send] = useMachine( combobox.machine({ onSelect(details) { // details => { label: string, value: string, relatedTarget: HTMLElement | null } 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", }), )
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="option"][data-disabled] { /* styles for option 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 */ }
Methods and Properties
The combobox's api
exposes the following methods:
isFocused
boolean
Whether the combobox is focusedisOpen
boolean
Whether the combobox content or listbox is openisInputValueEmpty
boolean
Whether the combobox input is emptyinputValue
string
The current value of the combobox inputfocusedOption
OptionData
The currently focused option (by pointer or keyboard)selectedValue
string
The currently selected option valuesetValue
(value: string | OptionData) => void
Function to set the combobox valuesetInputValue
(value: string) => void
Function to set the combobox input valueclearValue
() => void
Function to clear the combobox input value and selected valuefocus
() => void
Function to focus the combobox inputgetOptionState
(props: OptionProps) => { isDisabled: boolean; isHighlighted: boolean; isChecked: boolean; }
Returns the state of an option
Edit this page on GitHub