Skip to main content

A select component lets you pick a value from predefined options.

Loading...

Features

  • Supports single and multiple selection
  • Supports typeahead, keyboard navigation, and RTL
  • Supports controlled open, value, and highlight state
  • Supports form submission and browser autofill

Installation

Install the select package:

npm install @zag-js/select @zag-js/vue # or yarn add @zag-js/select @zag-js/vue

Anatomy

Check the select anatomy and part names.

Each part includes a data-part attribute to help identify them in the DOM.

Usage

Import the select package:

import * as select from "@zag-js/select"

These are the key exports:

  • machine - State machine logic.
  • connect - Maps machine state to JSX props and event handlers.
  • collection - Creates a collection interface from an array of items.

Then use the framework integration helpers:

<script setup> import * as select from "@zag-js/select" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, Teleport } from "vue" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, //... ] const service = useMachine(select.machine, { id: "1", collection: select.collection({ items: selectData, }), }) const api = computed(() => select.connect(service, normalizeProps)) </script> <template> <div> <label v-bind="api.getLabelProps()">Label</label> <button v-bind="api.getTriggerProps()"> <span>{{ api.valueAsString || "Select option" }}</span> <span></span> </button> </div> <Teleport to="body"> <div v-bind="api.getPositionerProps()"> <ul v-bind="api.getContentProps()"> <li v-for="item in selectData" :key="item.value" v-bind="api.getItemProps({ item })" > <span>{{ item.label }}</span> <span v-bind="api.getItemIndicatorProps({ item })"></span> </li> </ul> </div> </Teleport> </template>

Setting the initial value

Use the defaultValue property to set the initial value of the select.

The value property must be an array of strings. If selecting a single value, pass an array with a single string.

const collection = select.collection({ items: [ { label: "Nigeria", value: "ng" }, { label: "Ghana", value: "gh" }, { label: "Kenya", value: "ke" }, //... ], }) const service = useMachine(select.machine, { id: useId(), collection, defaultValue: ["ng"], })

Selecting multiple values

Set multiple to true to allow selecting multiple values.

const service = useMachine(select.machine, { id: useId(), collection, multiple: true, })

Controlled select value

Use value and onValueChange for controlled selection.

const service = useMachine(select.machine, { id: useId(), collection, value, onValueChange(details) { setValue(details.value) }, })

Using a custom object format

By default, the select 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.
  • groupBy — A function that returns the group of an item.
  • groupSort — An array or function to sort the groups.
const collection = select.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 }, groupBy(item) { return item.available ? "available" : "unavailable" }, groupSort: ["available", "unavailable"], }) // use the collection const service = useMachine(select.machine, { id: useId(), collection, })

Usage within a form

To use select in a form, set name and render api.getHiddenSelectProps().

<script setup> import * as select from "@zag-js/select" import { normalizeProps, useMachine } from "@zag-js/vue" import { Teleport } from "vue" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, //... ] const service = useMachine(select.machine, { id: "1", collection: select.collection({ items: selectData, }), name: "country", }) const api = computed(() => select.connect(service, normalizeProps)) </script> <template> <form> <!-- Hidden select --> <select v-bind="api.getHiddenSelectProps()"> <option v-for="item in selectData" :key="item.value" :value="item.value"> {{ item.label }} </option> </select> <!-- Custom Select --> <div v-bind="api.getControlProps()"> <label v-bind="api.getLabelProps()">Label</label> <button type="button" v-bind="api.getTriggerProps()"> <span>{{ api.valueAsString || "Select option" }}</span> <span></span> </button> </div> <Teleport to="body"> <div v-bind="api.getPositionerProps()"> <ul v-bind="api.getContentProps()"> <li v-for="item in selectData" :key="item.value" v-bind="api.getItemProps({ item })" > <span>{{ label }}</span> <span v-bind="api.getItemIndicatorProps({ item })"></span> </li> </ul> </div> </Teleport> </form> </template>

Browser autofill support

To support browser autofill for form fields like state or province, set autoComplete on the machine.

const service = useMachine(select.machine, { id: useId(), collection, name: "state", autoComplete: "address-level1", })

Disabling an item

To disable a select item, use itemToDisabled in the collection.

const collection = select.collection({ items: countries, itemToDisabled(item) { return item.disabled }, }) const service = useMachine(select.machine, { id: useId(), collection, })

Close on select

By default, the menu closes when you select an item with pointer, space, or enter. Set closeOnSelect to false to keep it open.

const service = useMachine(select.machine, { id: useId(), collection, closeOnSelect: false, })

Programmatic selection control

Use the API for imperative updates.

api.selectValue("ng") api.setValue(["ng", "ke"]) api.clearValue() // or api.clearValue("ng")

Controlling open state

Use open and onOpenChange for controlled open state, or defaultOpen for uncontrolled initial state.

const service = useMachine(select.machine, { id: useId(), collection, open, onOpenChange(details) { setOpen(details.open) // details => { open: boolean, value: string[] } }, })
const service = useMachine(select.machine, { id: useId(), collection, defaultOpen: true, })

Controlling highlighted item

Use highlightedValue and onHighlightChange to manage item highlight.

const service = useMachine(select.machine, { id: useId(), collection, highlightedValue, onHighlightChange(details) { setHighlightedValue(details.highlightedValue) // details => { highlightedValue, highlightedItem, highlightedIndex } }, })

Positioning the popup

Use positioning to control popup placement and behavior.

const service = useMachine(select.machine, { id: useId(), collection, positioning: { placement: "bottom-start" }, })

Looping the keyboard navigation

By default, arrow key navigation stops at the first and last options. Set loopFocus: true to loop back around.

const service = useMachine(select.machine, { id: useId(), collection, loopFocus: true, })

Allowing deselection in single-select mode

Set deselectable to allow clicking the selected item again to clear the value.

const service = useMachine(select.machine, { id: useId(), collection, deselectable: true, })

Listening for highlight changes

Use onHighlightChange to listen for highlighted item changes.

const service = useMachine(select.machine, { id: useId(), collection, onHighlightChange(details) { // details => { highlightedValue, highlightedItem, highlightedIndex } console.log(details) }, })

Listening for selection changes

Use onValueChange to listen for selected item changes.

const service = useMachine(select.machine, { id: useId(), collection, onValueChange(details) { // details => { value: string[], items: Item[] } console.log(details) }, })

Listening for item selection

Use onSelect when you need the selected item value immediately.

const service = useMachine(select.machine, { id: useId(), collection, onSelect(details) { // details => { value: string } console.log(details.value) }, })

Listening for open and close events

Use onOpenChange to listen for open and close events.

const service = useMachine(select.machine, { id: useId(), collection, onOpenChange(details) { // details => { open: boolean, value: string[] } console.log(details.open) }, })

Grouping items

The select relies on the collection, so rendered items must match collection items.

Set groupBy on the collection to define item groups.

const collection = select.collection({ items: [], itemToValue: (item) => item.value, itemToString: (item) => item.label, groupBy: (item) => item.group || "default", })

Then, use the collection.group() method to render the grouped items.

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

Usage with large data

For large lists, combine select with a virtualization library like react-window or @tanstack/react-virtual.

Example with @tanstack/react-virtual:

function Demo() { const selectData = [] const contentRef = useRef(null) const rowVirtualizer = useVirtualizer({ count: selectData.length, getScrollElement: () => contentRef.current, estimateSize: () => 32, }) const service = useMachine(select.machine, { id: useId(), collection, scrollToIndexFn(details) { rowVirtualizer.scrollToIndex(details.index, { align: "center", behavior: "auto", }) }, }) const api = select.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> {/* ... */} <Portal> <div {...api.getPositionerProps()}> <div ref={contentRef} {...api.getContentProps()}> <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, width: "100%", position: "relative", }} > {rowVirtualizer.getVirtualItems().map((virtualItem) => { const item = selectData[virtualItem.index] return ( <div key={item.value} {...api.getItemProps({ item })} style={{ position: "absolute", top: 0, left: 0, width: "100%", height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", }} > <span>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}></span> </div> ) })} </div> </div> </div> </Portal> </div> ) }

Usage within dialog

When using select in a dialog, avoid rendering it in a Portal or Teleport outside the dialog focus scope.

Styling guide

Each select part includes a data-part attribute you can target in CSS.

Open and closed state

When the select is open, the trigger and content is given a data-state attribute.

[data-part="trigger"][data-state="open|closed"] { /* styles for open or closed state */ } [data-part="content"][data-state="open|closed"] { /* styles for open or closed state */ }

Selected state

Items are given a data-state attribute, indicating whether they are selected.

[data-part="item"][data-state="checked|unchecked"] { /* styles for selected or unselected state */ }

Highlighted state

When an item is highlighted, via keyboard navigation or pointer, it is given a data-highlighted attribute.

[data-part="item"][data-highlighted] { /* styles for highlighted state */ }

Invalid state

When the select is invalid, the label and trigger is given a data-invalid attribute.

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

Disabled state

When the select is disabled, the trigger and label is given a data-disabled attribute.

[data-part="trigger"][data-disabled] { /* styles for disabled select state */ } [data-part="label"][data-disabled] { /* styles for disabled label state */ } [data-part="item"][data-disabled] { /* styles for disabled option state */ }

Optionally, when an item is disabled, it is given a data-disabled attribute.

Empty state

When no option is selected, the trigger is given a data-placeholder-shown attribute.

[data-part="trigger"][data-placeholder-shown] { /* styles for empty select state */ }

Methods and Properties

Machine Context

The select machine exposes the following context properties:

  • collectionListCollection<T>The item collection
  • idsPartial<{ root: string; content: string; control: string; trigger: string; clearTrigger: string; label: string; hiddenSelect: string; positioner: string; item: (id: string | number) => string; itemGroup: (id: string | number) => string; itemGroupLabel: (id: string | number) => string; }>The ids of the elements in the select. Useful for composition.
  • namestringThe `name` attribute of the underlying select.
  • formstringThe associate form of the underlying select.
  • autoCompletestringThe autocomplete attribute for the hidden select. Enables browser autofill (e.g. "address-level1" for state).
  • disabledbooleanWhether the select is disabled
  • invalidbooleanWhether the select is invalid
  • readOnlybooleanWhether the select is read-only
  • requiredbooleanWhether the select is required
  • closeOnSelectbooleanWhether the select should close after an item is selected
  • onSelect(details: SelectionDetails) => voidFunction called when an item is selected
  • onHighlightChange(details: HighlightChangeDetails<T>) => voidThe callback fired when the highlighted item changes.
  • onValueChange(details: ValueChangeDetails<T>) => voidThe callback fired when the selected item changes.
  • onOpenChange(details: OpenChangeDetails) => voidFunction called when the popup is opened
  • positioningPositioningOptionsThe positioning options of the menu.
  • valuestring[]The controlled keys of the selected items
  • defaultValuestring[]The initial default value of the select when rendered. Use when you don't need to control the value of the select.
  • highlightedValuestringThe controlled key of the highlighted item
  • defaultHighlightedValuestringThe initial value of the highlighted item when opened. Use when you don't need to control the highlighted value of the select.
  • loopFocusbooleanWhether to loop the keyboard navigation through the options
  • multiplebooleanWhether to allow multiple selection
  • openbooleanWhether the select menu is open
  • defaultOpenbooleanWhether the select's open state is controlled by the user
  • scrollToIndexFn(details: ScrollToIndexDetails) => voidFunction to scroll to a specific index
  • compositebooleanWhether the select is a composed with other composite widgets like tabs or combobox
  • deselectablebooleanWhether the value can be cleared by clicking the selected item. **Note:** this is only applicable for single selection
  • dir"ltr" | "rtl"The document's text/writing direction.
  • idstringThe 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 select api exposes the following methods:

  • focusedbooleanWhether the select is focused
  • openbooleanWhether the select is open
  • emptybooleanWhether the select value is empty
  • highlightedValuestringThe value of the highlighted item
  • highlightedItemVThe highlighted item
  • setHighlightValue(value: string) => voidFunction to highlight a value
  • clearHighlightValueVoidFunctionFunction to clear the highlighted value
  • selectedItemsV[]The selected items
  • hasSelectedItemsbooleanWhether there's a selected option
  • valuestring[]The selected item keys
  • valueAsStringstringThe string representation of the selected items
  • selectValue(value: string) => voidFunction to select a value
  • selectAllVoidFunctionFunction to select all values
  • setValue(value: string[]) => voidFunction to set the value of the select
  • clearValue(value?: string) => voidFunction to clear the value of the select. If a value is provided, it will only clear that value, otherwise, it will clear all values.
  • focusVoidFunctionFunction to focus on the select input
  • getItemState(props: ItemProps<any>) => ItemStateReturns the state of a select item
  • setOpen(open: boolean) => voidFunction to open or close the select
  • collectionListCollection<V>Function to toggle the select
  • reposition(options?: Partial<PositioningOptions>) => voidFunction to set the positioning options of the select
  • multiplebooleanWhether the select allows multiple selections
  • disabledbooleanWhether the select is disabled

Data Attributes

Root
data-scope
select
data-part
root
data-invalid
Present when invalid
data-readonly
Present when read-only
Label
data-scope
select
data-part
label
data-disabled
Present when disabled
data-invalid
Present when invalid
data-readonly
Present when read-only
data-required
Present when required
Control
data-scope
select
data-part
control
data-state
"open" | "closed"
data-focus
Present when focused
data-disabled
Present when disabled
data-invalid
Present when invalid
ValueText
data-scope
select
data-part
value-text
data-disabled
Present when disabled
data-invalid
Present when invalid
data-focus
Present when focused
Trigger
data-scope
select
data-part
trigger
data-state
"open" | "closed"
data-disabled
Present when disabled
data-invalid
Present when invalid
data-readonly
Present when read-only
data-placement
The placement of the trigger
data-placeholder-shown
Present when placeholder is shown
Indicator
data-scope
select
data-part
indicator
data-state
"open" | "closed"
data-disabled
Present when disabled
data-invalid
Present when invalid
data-readonly
Present when read-only
Item
data-scope
select
data-part
item
data-value
The value of the item
data-state
"checked" | "unchecked"
data-highlighted
Present when highlighted
data-disabled
Present when disabled
ItemText
data-scope
select
data-part
item-text
data-state
"checked" | "unchecked"
data-disabled
Present when disabled
data-highlighted
Present when highlighted
ItemIndicator
data-scope
select
data-part
item-indicator
data-state
"checked" | "unchecked"
ItemGroup
data-scope
select
data-part
item-group
data-disabled
Present when disabled
ClearTrigger
data-scope
select
data-part
clear-trigger
data-invalid
Present when invalid
Content
data-scope
select
data-part
content
data-state
"open" | "closed"
data-nested
listbox
data-has-nested
listbox
data-placement
The placement of the content
data-activedescendant
The id the active descendant of the content

CSS Variables

Arrow
--arrow-size
The size of the arrow
--arrow-size-half
Half the size of the arrow
--arrow-background
Use this variable to style the arrow background
--arrow-offset
The offset position of the arrow
Positioner
--reference-width
The width of the reference element
--reference-height
The height of the root
--available-width
The available width in viewport
--available-height
The available height in viewport
--x
The x position for transform
--y
The y position for transform
--z-index
The z-index value
--transform-origin
The transform origin for animations
Content
--layer-index
The index of the dismissable in the layer stack
--nested-layer-count
The number of nested selects
Backdrop
--layer-index
The index of the dismissable in the layer stack

Accessibility

Adheres to the ListBox WAI-ARIA design pattern.

Keyboard Interactions

  • Space
    When focus is on trigger, opens the select and focuses the first selected item.
    When focus is on the content, selects the highlighted item.
  • Enter
    When focus is on trigger, opens the select and focuses the first selected item.
    When focus is on content, selects the focused item.
  • ArrowDown
    When focus is on trigger, opens the select.
    When focus is on content, moves focus to the next item.
  • ArrowUp
    When focus is on trigger, opens the select.
    When focus is on content, moves focus to the previous item.
  • Esc
    Closes the select and moves focus to trigger.
  • A-Za-z
    When focus is on trigger, selects the item whose label starts with the typed character.
    When focus is on the listbox, moves focus to the next item with a label that starts with the typed character.
Edit this page on GitHub