Skip to main content
A select component lets you pick a value from predefined options. It supports single and multiple selection, typeahead, keyboard navigation, RTL, and form submission. Features
  • Single and multiple selection
  • Typeahead and full keyboard navigation
  • RTL support
  • Controlled open state, value, and highlighted item
  • Form submission and browser autofill via hidden <select>
  • Custom object formats via collection adapters
  • Virtualized lists via scrollToIndexFn

Installation

npm install @zag-js/select @zag-js/react

Usage

import { useMachine, normalizeProps } from "@zag-js/react"
import * as select from "@zag-js/select"
import { useId } from "react"

const countries = [
  { label: "Nigeria", value: "ng" },
  { label: "Ghana", value: "gh" },
  { label: "Kenya", value: "ke" },
]

const collection = select.collection({ items: countries })

export function Select() {
  const service = useMachine(select.machine, {
    id: useId(),
    collection,
  })
  const api = select.connect(service, normalizeProps)

  return (
    <div {...api.getRootProps()}>
      <label {...api.getLabelProps()}>Country</label>
      <div {...api.getControlProps()}>
        <button {...api.getTriggerProps()}>
          <span {...api.getValueTextProps()}>
            {api.empty ? "Select a country" : api.valueAsString}
          </span>
        </button>
      </div>
      <div {...api.getPositionerProps()}>
        <ul {...api.getContentProps()}>
          {countries.map((item) => (
            <li key={item.value} {...api.getItemProps({ item })}>
              <span {...api.getItemTextProps({ item })}>{item.label}</span>
              <span {...api.getItemIndicatorProps({ item })}></span>
            </li>
          ))}
        </ul>
      </div>
      <select {...api.getHiddenSelectProps()} />
    </div>
  )
}

Setting the initial value

Pass defaultValue as an array of string values. Even for single selection, use an array.
const service = useMachine(select.machine, {
  id: useId(),
  collection,
  defaultValue: ["ng"],
})

Multiple selection

Set multiple: true to allow selecting more than one item.
const service = useMachine(select.machine, {
  id: useId(),
  collection,
  multiple: true,
})

Controlled value

Use value and onValueChange for controlled selection state.
const service = useMachine(select.machine, {
  id: useId(),
  collection,
  value,
  onValueChange(details) {
    // details => { value: string[], items: Item[] }
    setValue(details.value)
  },
})

Custom object format

Pass itemToString, itemToValue, and itemToDisabled to the collection function when your items don’t have label/value properties.
const collection = select.collection({
  items: [
    { id: 1, fruit: "Banana", available: true },
    { id: 2, fruit: "Apple", available: false },
  ],
  itemToString: (item) => item.fruit,
  itemToValue: (item) => String(item.id),
  itemToDisabled: (item) => !item.available,
})

Grouping items

Use groupBy on the collection and collection.group() when rendering.
const collection = select.collection({
  items,
  itemToValue: (item) => item.value,
  itemToString: (item) => item.label,
  groupBy: (item) => item.category,
})

// Rendering grouped items
collection.group().map(([group, items]) => (
  <div key={group}>
    <div {...api.getItemGroupProps({ id: group })}>{group}</div>
    {items.map((item) => (
      <div key={item.value} {...api.getItemProps({ item })}>
        <span {...api.getItemTextProps({ item })}>{item.label}</span>
        <span {...api.getItemIndicatorProps({ item })}></span>
      </div>
    ))}
  </div>
))

Usage within a form

Set name and render the hidden select to support form submission and browser autofill.
const service = useMachine(select.machine, {
  id: useId(),
  collection,
  name: "country",
  autoComplete: "country",
})

// In JSX
<select {...api.getHiddenSelectProps()} />

Controlling open state

Use open/onOpenChange for controlled open state, or defaultOpen for initial state.
const service = useMachine(select.machine, {
  id: useId(),
  collection,
  open,
  onOpenChange(details) {
    setOpen(details.open)
  },
})

Allowing deselection

Set deselectable to allow clicking the selected item to clear the value (single selection only).
const service = useMachine(select.machine, {
  id: useId(),
  collection,
  deselectable: true,
})

API reference

collection
ListCollection
required
The item collection created with select.collection(...). Required.
defaultValue
string[]
The initial selected values (uncontrolled).
value
string[]
The controlled selected values. Use with onValueChange.
multiple
boolean
Whether to allow multiple items to be selected.
deselectable
boolean
Whether clicking the selected item clears the value (single selection only).
closeOnSelect
boolean
Whether the dropdown closes when an item is selected. Defaults to true.
loopFocus
boolean
Whether keyboard navigation wraps from the last to first item. Defaults to false.
open
boolean
The controlled open state of the dropdown.
defaultOpen
boolean
The initial open state (uncontrolled).
highlightedValue
string | null
The controlled highlighted item value.
disabled
boolean
Whether the select is disabled.
invalid
boolean
Whether the select is in an invalid state.
readOnly
boolean
Whether the select is read-only.
name
string
The name attribute for form submission.
autoComplete
string
The autocomplete attribute on the hidden select for browser autofill.
positioning
PositioningOptions
Positioning options for the dropdown (placement, offset, etc.).
scrollToIndexFn
(details: ScrollToIndexDetails) => void
A function to scroll to a specific index. Use with virtualization libraries.
onValueChange
(details: { value: string[], items: Item[] }) => void
Callback fired when the selection changes.
onHighlightChange
(details: HighlightChangeDetails) => void
Callback fired when the highlighted item changes.
onOpenChange
(details: { open: boolean, value: string[] }) => void
Callback fired when the dropdown opens or closes.

Styling

Each part has a data-part attribute for CSS targeting.

Open and closed state

[data-part="trigger"][data-state="open"] { /* trigger when open */ }
[data-part="trigger"][data-state="closed"] { /* trigger when closed */ }
[data-part="content"][data-state="open"] { /* content when open */ }
[data-part="content"][data-state="closed"] { /* content when closed */ }

Selected and highlighted items

[data-part="item"][data-state="checked"] { /* selected item */ }
[data-part="item"][data-state="unchecked"] { /* unselected item */ }
[data-part="item"][data-highlighted] { /* keyboard/pointer highlighted item */ }

Disabled and invalid states

[data-part="trigger"][data-disabled] { /* disabled trigger */ }
[data-part="label"][data-disabled] { /* disabled label */ }
[data-part="item"][data-disabled] { /* disabled item */ }

[data-part="trigger"][data-invalid] { /* invalid trigger */ }
[data-part="label"][data-invalid] { /* invalid label */ }

Empty (placeholder) state

[data-part="trigger"][data-placeholder-shown] {
  /* styles when no value is selected */
}

Accessibility

Adheres to the ListBox WAI-ARIA design pattern.

Keyboard interactions

KeyDescription
Space / EnterOpens the dropdown or selects the highlighted item
ArrowDownHighlights the next item; opens dropdown if closed
ArrowUpHighlights the previous item; opens dropdown if closed
HomeHighlights the first item
EndHighlights the last item
EscapeCloses the dropdown without changing selection
A–Z, a–zTypeahead to jump to matching item

Build docs developers (and LLMs) love