Skip to main content
0.41.0
View Zag.js on Github
Join the Discord server

Select

A Select component allows users pick a value from predefined options.

Properties

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

This command will install the framework agnostic menu logic and the reactive utilities for your framework of choice.

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-part attribute 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 id to the useMachine hook. 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 [state, send] = useMachine( select.machine({ id: useId(), collection, }), ) const api = select.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <div {...api.controlProps}> <label {...api.labelProps}>Label</label> <button {...api.triggerProps}> {api.valueAsString || "Select option"} </button> </div> <Portal> <div {...api.positionerProps}> <ul {...api.contentProps}> {selectData.map((item) => ( <li key={item.value} {...api.getItemProps({ item })}> <span>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}></span> </li> ))} </ul> </div> </Portal> </div> ) }

Setting the initial value

To set the initial value of the select, pass the value property to the select machine's context.

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 [state, send] = useMachine( select.machine({ id: useId(), collection, value: ["ng"], }), )

Selecting multiple values

To allow selecting multiple values, set the multiple property in the machine's context to true.

const [state, send] = 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.
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 }, }) // use the collection const [state, send] = useMachine( select.machine({ id: useId(), collection, }), )

Usage within a form

To use select within a form, you'll need to:

  • Pass the name property to the select machine's context
  • Render a hidden select element using api.selectProps
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 [state, send] = useMachine( select.machine({ id: useId(), collection: select.collection({ items: selectData }) name: "country", }), ) const api = select.connect(state, send, normalizeProps) return ( <form> {/* Hidden select */} <select {...api.hiddenSelectProps}> {selectData.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> {/* Custom Select */} <div {...api.controlProps}> <label {...api.labelProps}>Label</label> <button type="button" {...api.triggerProps}> <span>{api.valueAsString || "Select option"}</span> <CaretIcon /> </button> </div> <Portal> <div {...api.positionerProps}> <ul {...api.contentProps}> {selectData.map((item) => ( <li key={item.value} {...api.getItemProps({ item })}> <span>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}></span> </li> ))} </ul> </div> </Portal> </form> ) }

Selecting option on blur

Use the selectOnBlur property to allow selecting the highlighted option when immediately interacting or focusing outside the select (e.g. using the Tab key).

const [state, send] = useMachine( select.machine({ id: useId(), collection, selectOnBlur: true, }), )

Disabling the select

To disable the select, set the disabled property in the machine's context to true.

const [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = useMachine( select.machine({ id: useId(), collection, onOpenChange(details) { // details => { open: boolean } console.log("Select opened") }, }), )

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 [state, send] = useMachine( select.machine({ id: useId(), collection, scrollToIndexFn(details) { rowVirtualizer.scrollToIndex(details.index, { align: "center", behavior: "auto", }) }, }), ) const api = select.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> {/* ... */} <Portal> <div {...api.positionerProps}> <div ref={contentRef} {...api.contentProps}> <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, you'll need to 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.

Consider designing a portalled property in your component to allow you decide where to render the select in a portal.

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-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:

  • collectionCollection<any>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.
  • disabledbooleanWhether the select is disabled
  • invalidbooleanWhether the select is invalid
  • readOnlybooleanWhether the select is read-only
  • closeOnSelectbooleanWhether the select should close after an item is selected
  • selectOnBlurbooleanWhether to select the highlighted item when the user presses Tab, and the menu is open.
  • 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 keys of the selected items
  • highlightedValuestringThe key of the highlighted item
  • loopbooleanWhether to loop the keyboard navigation through the options
  • multiplebooleanWhether to allow multiple selection
  • openbooleanWhether the select menu is open
  • open.controlledbooleanWhether the select's open state is controlled by the user
  • scrollToIndexFn(details: ScrollToIndexDetails) => voidFunction to scroll to a specific index
  • 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:

  • isFocusedbooleanWhether the select is focused
  • isOpenbooleanWhether the select is open
  • isValueEmptybooleanWhether the select value is empty
  • highlightedValuestringThe value of the highlighted item
  • highlightedItemVThe highlighted item
  • highlightValue(value: string) => voidThe value of the select input
  • 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
  • setValue(value: string[]) => voidFunction to set the value of the select
  • clearValue(value?: string) => voidFunction to clear the value of the select
  • focus() => voidFunction to focus on the select input
  • getItemState(props: ItemProps<any>) => ItemStateReturns the state of a select item
  • open() => voidFunction to open the select
  • close() => voidFunction to close the select
  • collectionCollection<V>Function to toggle the select
  • setCollection(collection: Collection<V>) => voidFunction to set the collection of items
  • reposition(options: Partial<PositioningOptions>) => voidFunction to set the positioning options of the select

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

On this page