Skip to main content
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

Usage

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>
  )
}

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)
  },
})

Configuring popup trigger behavior

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
})

Positioning the popup

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.
value
string[]
The controlled selected value(s).
defaultValue
string[]
default:"[]"
The initial selected value(s) when rendered.
inputValue
string
The controlled value of the combobox input.
defaultInputValue
string
default:"\"\""
The initial value of the combobox input when rendered.
multiple
boolean
Whether to allow multiple selection.
disabled
boolean
Whether the combobox is disabled.
readOnly
boolean
Whether the combobox is read-only.
invalid
boolean
Whether the combobox is in an invalid state.
required
boolean
Whether the combobox is required.
placeholder
string
Placeholder text for the input.
allowCustomValue
boolean
Whether users can type a value not present in the collection.
closeOnSelect
boolean
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.
positioning
PositioningOptions
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.

Parts

PartDescription
rootOutermost wrapper
labelThe label element
controlWraps the input and trigger button
inputThe text input
triggerThe dropdown toggle button
clear-triggerButton to clear the selection
positionerPositions the popup
contentThe popup listbox
itemAn individual list item
item-textThe text inside a list item
item-indicatorSelected 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

KeyDescription
ArrowDownOpens the popup and moves highlight to the next item.
ArrowUpOpens the popup and moves highlight to the previous item.
EnterSelects the highlighted item and closes the popup.
EscapeCloses the popup and clears the input (if allowCustomValue is false).
HomeMoves highlight to the first item.
EndMoves highlight to the last item.
Alt + ArrowDownOpens the popup without moving highlight.
Alt + ArrowUpCloses the popup if open.

Build docs developers (and LLMs) love