Select
A Select component allows users pick a value from predefined options.
- Nigeria
- Japan
- Korea
- Kenya
- United Kingdom
Features
- Support for selecting a single or multiple option
- Keyboard support for opening the listbox using the arrow keys, including automatically focusing the first or last item.
- Support for looping keyboard navigation.
- Support for selecting an item on blur.
- Typeahead to allow selecting options by typing text, even without opening the listbox
- Support for Right to Left direction.
Installation
To use the select machine in your project, run the following command in your command line:
npm install @zag-js/select @zag-js/react # or yarn add @zag-js/select @zag-js/react
npm install @zag-js/select @zag-js/solid # or yarn add @zag-js/select @zag-js/solid
npm install @zag-js/select @zag-js/vue # or yarn add @zag-js/select @zag-js/vue
npm install @zag-js/select @zag-js/svelte # or yarn add @zag-js/select @zag-js/svelte
Anatomy
To set up the select correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the select package into your project
import * as select from "@zag-js/select"
The select package exports these functions:
machine— The state machine logic for the select.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.
You'll need to provide a unique
idto theuseMachinehook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the select machine in your project 🔥
import * as select from "@zag-js/select" import { useMachine, normalizeProps, Portal } from "@zag-js/react" import { useId, useRef } from "react" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, { label: "Korea", value: "KO" }, { label: "Kenya", value: "KE" }, { label: "United Kingdom", value: "UK" }, { label: "Ghana", value: "GH" }, { label: "Uganda", value: "UG" }, ] export function Select() { const collection = select.collection({ items: selectData, itemToString: (item) => item.label, itemToValue: (item) => item.value, }) const service = useMachine(select.machine, { id: useId(), collection, }) const api = select.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <div {...api.getControlProps()}> <label {...api.getLabelProps()}>Label</label> <button {...api.getTriggerProps()}> {api.valueAsString || "Select option"} </button> </div> <Portal> <div {...api.getPositionerProps()}> <ul {...api.getContentProps()}> {selectData.map((item) => ( <li key={item.value} {...api.getItemProps({ item })}> <span>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> ))} </ul> </div> </Portal> </div> ) }
import * as select from "@zag-js/select" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, //... ] export function Select() { const service = useMachine(select.machine, { id: createUniqueId(), collection: select.collection({ items: selectData, }), }) const api = createMemo(() => select.connect(service, normalizeProps)) return ( <div> <div> <label {...api().getLabelProps()}>Label</label> <button {...api().getTriggerProps()}> <span>{api().valueAsString || "Select option"}</span> </button> </div> <div {...api().getPositionerProps()}> <ul {...api().getContentProps()}> {selectData.map((item) => ( <li key={item.value} {...api().getItemProps({ item })}> <span>{item.label}</span> <span {...api().getItemIndicatorProps({ item })}>✓</span> </li> ))} </ul> </div> </div> ) }
<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>
<script lang="ts"> import * as select from "@zag-js/select" import { portal, useMachine, normalizeProps } from "@zag-js/svelte" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, { label: "Korea", value: "KO" }, { label: "Kenya", value: "KE" }, { label: "United Kingdom", value: "UK" }, { label: "Ghana", value: "GH" }, { label: "Uganda", value: "UG" }, ] const collection = select.collection({ items: selectData, itemToString: (item) => item.label, itemToValue: (item) => item.value, }) const id = $props.id() const service = useMachine(select.machine, { id, collection, }) const api = $derived(select.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <div {...api.getControlProps()}> <label {...api.getLabelProps()}>Label</label> <button {...api.getTriggerProps()}> {api.valueAsString || "Select option"} </button> </div> <div use:portal {...api.getPositionerProps()}> <ul {...api.getContentProps()}> {#each selectData as item} <li {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> {/each} </ul> </div> </div>
Setting the initial value
Use the defaultValue property to set the initial value of the select.
The
valueproperty 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
To allow selecting multiple values, set the multiple property to true.
const service = useMachine(select.machine, { id: useId(), collection, multiple: true, })
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 within a form, you'll need to:
- Pass the
nameproperty to the select machine's context - Render a hidden
selectelement usingapi.getSelectProps()
import * as select from "@zag-js/select" import { useMachine, normalizeProps, Portal } from "@zag-js/react" import { useId } from "react" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, { label: "Korea", value: "KO" }, { label: "Kenya", value: "KE" }, { label: "United Kingdom", value: "UK" }, { label: "Ghana", value: "GH" }, { label: "Uganda", value: "UG" }, ] export function SelectWithForm() { const service = useMachine(select.machine, { id: useId(), collection: select.collection({ items: selectData }), name: "country", }) const api = select.connect(service, normalizeProps) return ( <form> {/* Hidden select */} <select {...api.getHiddenSelectProps()}> {selectData.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> {/* Custom Select */} <div {...api.getControlProps()}> <label {...api.getLabelProps()}>Label</label> <button type="button" {...api.getTriggerProps()}> <span>{api.valueAsString || "Select option"}</span> <CaretIcon /> </button> </div> <Portal> <div {...api.getPositionerProps()}> <ul {...api.getContentProps()}> {selectData.map((item) => ( <li key={item.value} {...api.getItemProps({ item })}> <span>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> ))} </ul> </div> </Portal> </form> ) }
import * as select from "@zag-js/select" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, //... ] export function SelectWithForm() { const service = useMachine( select.machine, ({ collection: select.collection({ items: selectData, }), id: createUniqueId(), name: "country", }), )Ø const api = createMemo(() => select.connect(service, normalizeProps)) return ( <form> <div {...api().getRootProps()}> {/* Hidden select */} <select {...api().getHiddenSelectProps()}> {selectData.map((item) => ( <option key={item.value} value={item.value}> {item.label} </option> ))} </select> {/* Custom Select */} <div {...api().getControlProps()}> <label {...api().getLabelProps()}>Label</label> <button type="button" {...api().getTriggerProps()}> <span>{api().valueAsString || "Select option"}</span> <CaretIcon /> </button> </div> <Portal> <div {...api().getPositionerProps()}> <ul {...api().getContentProps()}> {selectData.map((item) => ( <li key={item.value} {...api().getItemProps({ item })}> <span>{item.label}</span> <span {...api().getItemIndicatorProps({ item })}>✓</span> </li> ))} </ul> </div> </Portal> </div> </form> ) }
<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>
<script lang="ts"> import * as select from "@zag-js/select" import { portal, normalizeProps, useMachine } from "@zag-js/svelte" const selectData = [ { label: "Nigeria", value: "NG" }, { label: "Japan", value: "JP" }, { label: "Korea", value: "KO" }, { label: "Kenya", value: "KE" }, { label: "United Kingdom", value: "UK" }, { label: "Ghana", value: "GH" }, { label: "Uganda", value: "UG" }, ] const service = useMachine(select.machine, { id: "1", collection: select.collection({ items: selectData }), name: "country", }) const api = $derived(select.connect(service, normalizeProps)) </script> <form> <!-- Hidden select --> <select {...api.getHiddenSelectProps()}> {#each selectData as option} <option value={option.value}> {option.label} </option> {/each} </select> <!-- Custom Select --> <div {...api.getControlProps()}> <label {...api.getLabelProps()}>Label</label> <button type="button" {...api.getTriggerProps()}> <span>{api.valueAsString || "Select option"}</span> <CaretIcon /> </button> </div> <div use:portal {...api.getPositionerProps()}> <ul {...api.getContentProps()}> {#each selectData as item} <li {...api.getItemProps({ item })}> <span>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> {/each} </ul> </div> </form>
Disabling the select
To disable the select, set the disabled property in the machine's context to
true.
const service = useMachine(select.machine, { id: useId(), collection, disabled: true, })
Disabling an item
To make a combobox option disabled, pass the isItemDisabled property to the
collection function.
const collection = select.collection({ items: countries, isItemDisabled(item) { return item.disabled }, }) const service = useMachine(select.machine, { id: useId(), collection, })
Close on select
This behaviour ensures that the menu is closed when an item is selected and is
true by default. It's only concerned with when an item is selected with
pointer, space key or enter key.
To disable the behaviour, set the closeOnSelect property in the machine's
context to false.
const service = useMachine(select.machine, { id: useId(), collection, closeOnSelect: false, })
Looping the keyboard navigation
When navigating with the select using the arrow down and up keys, the select
stops at the first and last options. If you need want the navigation to loop
back to the first or last option, set the loop: true in the machine's context.
const service = useMachine(select.machine, { id: useId(), collection, loop: true, })
Listening for highlight changes
When an item is highlighted with the pointer or keyboard, use the
onHighlightChange to listen for the change and do something with it.
const service = useMachine(select.machine, { id: useId(), onHighlightChange(details) { // details => { highlightedValue: string | null, highlightedItem: CollectionItem | null } console.log(details) }, })
Listening for selection changes
When an item is selected, use the onValueChange property to listen for the
change and do something with it.
const service = useMachine(select.machine, { id: useId(), collection, onValueChange(details) { // details => { value: string[], items: Item[] } console.log(details) }, })
Listening for open and close events
When the select is opened or closed, the onOpenChange callback is called. You
can listen for these events and do something with it.
const service = useMachine(select.machine, { id: useId(), collection, onOpenChange(details) { // details => { open: boolean } console.log("Select opened") }, })
Grouping items
The select component relies explicitly on the collection. This means the rendered items much match the items in the collection.
To ensure this, you need to pass the groupBy option to the collection
function.
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
Combine the select machine with the virtualization library like react-window
or @tanstack/react-virtual to handle large data.
Here's an example using @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 the select within a dialog, avoid rendering the select in a Portal
or Teleport. This is because the dialog will trap focus within it, and the
select will be rendered outside the dialog.
Styling guide
Earlier, we mentioned that each select part has a data-part attribute added to
them to select and style them in the DOM.
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-disabledattribute.
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 collectionidsPartial<{ 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.disabledbooleanWhether the select is disabledinvalidbooleanWhether the select is invalidreadOnlybooleanWhether the select is read-onlyrequiredbooleanWhether the select is requiredcloseOnSelectbooleanWhether the select should close after an item is selectedonSelect(details: SelectionDetails) => voidFunction called when an item is selectedonHighlightChange(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 openedpositioningPositioningOptionsThe positioning options of the menu.valuestring[]The controlled keys of the selected itemsdefaultValuestring[]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 itemdefaultHighlightedValuestringThe 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 optionsmultiplebooleanWhether to allow multiple selectionopenbooleanWhether the select menu is opendefaultOpenbooleanWhether the select's open state is controlled by the userscrollToIndexFn(details: ScrollToIndexDetails) => voidFunction to scroll to a specific indexcompositebooleanWhether the select is a composed with other composite widgets like tabs or comboboxdeselectablebooleanWhether the value can be cleared by clicking the selected item. **Note:** this is only applicable for single selectiondir"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 componentonFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the componentonInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component
Machine API
The select api exposes the following methods:
focusedbooleanWhether the select is focusedopenbooleanWhether the select is openemptybooleanWhether the select value is emptyhighlightedValuestringThe value of the highlighted itemhighlightedItemVThe highlighted itemsetHighlightValue(value: string) => voidFunction to highlight a valueclearHighlightValueVoidFunctionFunction to clear the highlighted valueselectedItemsV[]The selected itemshasSelectedItemsbooleanWhether there's a selected optionvaluestring[]The selected item keysvalueAsStringstringThe string representation of the selected itemsselectValue(value: string) => voidFunction to select a valueselectAllVoidFunctionFunction to select all valuessetValue(value: string[]) => voidFunction to set the value of the selectclearValue(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 inputgetItemState(props: ItemProps<any>) => ItemStateReturns the state of a select itemsetOpen(open: boolean) => voidFunction to open or close the selectcollectionListCollection<V>Function to toggle the selectreposition(options?: Partial<PositioningOptions>) => voidFunction to set the positioning options of the selectmultiplebooleanWhether the select allows multiple selectionsdisabledbooleanWhether the select is disabled
Data Attributes
Accessibility
Adheres to the ListBox WAI-ARIA design pattern.
Keyboard Interactions
- SpaceWhen focus is on trigger, opens the select and focuses the first selected item.
When focus is on the content, selects the highlighted item. - EnterWhen focus is on trigger, opens the select and focuses the first selected item.
When focus is on content, selects the focused item. - ArrowDownWhen focus is on trigger, opens the select.
When focus is on content, moves focus to the next item. - ArrowUpWhen focus is on trigger, opens the select.
When focus is on content, moves focus to the previous item. - EscCloses the select and moves focus to trigger.
- A-Za-zWhen 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