Listbox
A listbox component that displays a list of selectable options, supporting both single and multiple selection modes.
Favorites
Others
Features
- Supports single, multiple, or no selection
- Can be controlled or uncontrolled
- Fully managed keyboard navigation (arrow keys, home, end, etc.)
- Vertical and horizontal orientation
- Typeahead to allow focusing the matching item
- Supports items, labels, groups of items
- Supports grid and list layouts
Installation
To use the listbox machine in your project, run the following command in your command line:
npm install @zag-js/listbox @zag-js/react # or yarn add @zag-js/listbox @zag-js/react
npm install @zag-js/listbox @zag-js/solid # or yarn add @zag-js/listbox @zag-js/solid
npm install @zag-js/listbox @zag-js/vue # or yarn add @zag-js/listbox @zag-js/vue
npm install @zag-js/listbox @zag-js/svelte # or yarn add @zag-js/listbox @zag-js/svelte
Anatomy
To set up the listbox 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 listbox package into your project
import * as listbox from "@zag-js/listbox"
The listbox package exports two key functions:
machine— The state machine logic for the listbox widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
You'll also 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 listbox machine in your project 🔥
import * as listbox from "@zag-js/listbox" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" const data = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] function Listbox() { const collection = listbox.collection({ items: data }) const service = useMachine(listbox.machine, { id: useId(), collection, }) const api = listbox.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Select country</label> <ul {...api.getContentProps()}> {data.map((item) => ( <li key={item.value} {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> ))} </ul> </div> ) }
import * as listbox from "@zag-js/listbox" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId, For } from "solid-js" const data = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] function Listbox() { const collection = listbox.collection({ items: data }) const service = useMachine(listbox.machine, { id: createUniqueId(), collection, }) const api = createMemo(() => listbox.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Select country</label> <ul {...api().getContentProps()}> <For each={data}> {(item) => ( <li {...api().getItemProps({ item })}> <span {...api().getItemTextProps({ item })}>{item.label}</span> <span {...api().getItemIndicatorProps({ item })}>✓</span> </li> )} </For> </ul> </div> ) }
<script setup lang="ts"> import * as listbox from "@zag-js/listbox" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed, useId } from "vue" const data = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] const collection = listbox.collection({ items: data }) const service = useMachine(listbox.machine, { id: useId(), collection, }) const api = computed(() => listbox.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Select country</label> <ul v-bind="api.getContentProps()"> <li v-for="item in data" :key="item.value" v-bind="api.getItemProps({ item })" > <span v-bind="api.getItemTextProps({ item })">{{ item.label }}</span> <span v-bind="api.getItemIndicatorProps({ item })">✓</span> </li> </ul> </div> </template>
<script lang="ts"> import * as listbox from "@zag-js/listbox" import { normalizeProps, useMachine } from "@zag-js/svelte" import { createUniqueId } from "@zag-js/utils" const data = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] const collection = listbox.collection({ items: data }) const service = useMachine(listbox.machine, { id: createUniqueId(), collection, }) const api = $derived(listbox.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Select country</label> <ul {...api.getContentProps()}> {#each data as item} <li {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> {/each} </ul> </div>
Setting the initial selection
To set the initial selection, you can use the defaultValue property.
const service = useMachine(listbox.machine, { // ... defaultValue: ["item-1", "item-2"], })
Controlling the selection
To control the selection programmatically, you can use the value and
onValueChange properties.
const service = useMachine(listbox.machine, { value: ["item-1", "item-2"], onValueChange: (value) => { console.log(value) }, })
Filtering
The listbox component supports filtering of items via api.getInputProps.
Here's an example of how to support searching through a list of items.
import * as listbox from "@zag-js/listbox" import { createFilter } from "@zag-js/i18n-utils" import { normalizeProps, useMachine } from "@zag-js/react" import { useId, useMemo, useState } from "react" interface Item { label: string value: string } const data: Item[] = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] const filter = createFilter({ sensitivity: "base" }) function ListboxFiltering() { const [search, setSearch] = useState("") const collection = useMemo(() => { const items = data.filter((item) => filter.startsWith(item.label, search)) return listbox.collection({ items }) }, [search]) const service = useMachine(listbox.machine as listbox.Machine<Item>, { collection, id: useId(), typeahead: false, }) const api = listbox.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <input {...api.getInputProps({ autoHighlight: true })} onChange={(e) => setSearch(e.target.value)} value={search} /> <ul {...api.getContentProps()}> {collection.items.map((item) => ( <li key={item.value} {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> ))} </ul> </div> ) }
import * as listbox from "@zag-js/listbox" import { createFilter } from "@zag-js/i18n-utils" import { normalizeProps, useMachine } from "@zag-js/solid" import { createSignal, createMemo, createUniqueId, For } from "solid-js" interface Item { label: string value: string } const data: Item[] = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] const filter = createFilter({ sensitivity: "base" }) function ListboxFiltering() { const [search, setSearch] = createSignal("") const collection = createMemo(() => { const items = data.filter((item) => filter.startsWith(item.label, search())) return listbox.collection({ items }) }) const service = useMachine(listbox.machine as listbox.Machine<Item>, { get collection() { return collection() }, id: createUniqueId(), typeahead: false, }) const api = createMemo(() => listbox.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <input {...api().getInputProps({ autoHighlight: true })} onInput={(e) => setSearch(e.currentTarget.value)} value={search()} /> <ul {...api().getContentProps()}> <For each={collection().items}> {(item) => ( <li {...api().getItemProps({ item })}> <span {...api().getItemTextProps({ item })}>{item.label}</span> <span {...api().getItemIndicatorProps({ item })}>✓</span> </li> )} </For> </ul> </div> ) }
<script setup lang="ts"> import * as listbox from "@zag-js/listbox" import { createFilter } from "@zag-js/i18n-utils" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, ref } from "vue" import { useId } from "@zag-js/vue-aria" interface Item { label: string value: string } const data: Item[] = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] const filter = createFilter({ sensitivity: "base" }) const search = ref("") const collection = computed(() => { const items = data.filter((item) => filter.startsWith(item.label, search.value), ) return listbox.collection({ items }) }) const service = useMachine(listbox.machine as listbox.Machine<Item>, { id: useId(), get collection() { return collection.value }, typeahead: false, }) const api = computed(() => listbox.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <input v-bind="api.getInputProps({ autoHighlight: true })" v-model="search" /> <ul v-bind="api.getContentProps()"> <li v-for="item in collection.items" :key="item.value" v-bind="api.getItemProps({ item })" > <span v-bind="api.getItemTextProps({ item })">{{ item.label }}</span> <span v-bind="api.getItemIndicatorProps({ item })">✓</span> </li> </ul> </div> </template>
<script lang="ts"> import * as listbox from "@zag-js/listbox" import { createFilter } from "@zag-js/i18n-utils" import { normalizeProps, useMachine } from "@zag-js/svelte" interface Item { label: string value: string } const data: Item[] = [ { label: "Nigeria", value: "NG" }, { label: "United States", value: "US" }, { label: "Canada", value: "CA" }, { label: "Japan", value: "JP" }, ] const filter = createFilter({ sensitivity: "base" }) let search = $state("") const collection = $derived.by(() => { const items = data.filter((item) => filter.startsWith(item.label, search)) return listbox.collection({ items }) }) const id = $props.id() const service = useMachine(listbox.machine as listbox.Machine<Item>, { id, get collection() { return collection }, typeahead: false, }) const api = $derived(listbox.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <input {...api.getInputProps({ autoHighlight: true })} bind:value={search} /> <ul {...api.getContentProps()}> {#each collection.items as item (item.value)} <li {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </li> {/each} </ul> </div>
Selecting multiple items
To enable multiple selection, set the selectionMode property to multiple or
extended.
const service = useMachine(listbox.machine, { // ... selectionMode: "multiple", })
Selection Modes
By default, a user can select a single item in a listbox. You can set the
selectionMode property to a SelectionMode enumeration value to enable
multi-selection. Here are the selection mode values.
- single: A user can select a single item using the space bar, mouse click, or touch tap.
- multiple: A user can select multiple items using the space bar, mouse click, or touch tap to toggle selection on the focused item. Using the arrow keys, a user can move focus independently of selection.
- extended: With no modifier keys like
Ctrl,CmdorShift: the behavior is the same as single selection.
const service = useMachine(listbox.machine, { // ... selectionMode: "extended", })
Disabling items
To disable an item, you can use the disabled property.
api.getItemProps({ // ... disabled: true, })
To disable the entire listbox, you can use the disabled property.
const service = useMachine(listbox.machine, { disabled: true, })
Grid layout
To enable a grid layout, provide a grid collection to the collection property.
const service = useMachine(listbox.machine, { collection: listbox.gridCollection({ items: [], columnCount: 3, }), })
import * as listbox from "@zag-js/listbox" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" const data = [ { label: "Red", value: "red" }, { label: "Green", value: "green" }, { label: "Blue", value: "blue" }, { label: "Yellow", value: "yellow" }, { label: "Purple", value: "purple" }, { label: "Orange", value: "orange" }, ] function ListboxGrid() { const collection = listbox.gridCollection({ items: data, columnCount: 3, }) const service = useMachine(listbox.machine, { id: useId(), collection, }) const api = listbox.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Select color</label> <div {...api.getContentProps()} style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "8px", }} > {collection.items.map((item) => ( <div key={item.value} {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </div> ))} </div> </div> ) }
import * as listbox from "@zag-js/listbox" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId, For } from "solid-js" const data = [ { label: "Red", value: "red" }, { label: "Green", value: "green" }, { label: "Blue", value: "blue" }, { label: "Yellow", value: "yellow" }, { label: "Purple", value: "purple" }, { label: "Orange", value: "orange" }, ] function ListboxGrid() { const collection = listbox.gridCollection({ items: data, columnCount: 3, }) const service = useMachine(listbox.machine, { id: createUniqueId(), collection, }) const api = createMemo(() => listbox.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Select color</label> <div {...api().getContentProps()} style={{ display: "grid", "grid-template-columns": "repeat(3, 1fr)", gap: "8px", }} > <For each={collection.items}> {(item) => ( <div {...api().getItemProps({ item })}> <span {...api().getItemTextProps({ item })}>{item.label}</span> <span {...api().getItemIndicatorProps({ item })}>✓</span> </div> )} </For> </div> </div> ) }
<script setup lang="ts"> import * as listbox from "@zag-js/listbox" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed, useId } from "vue" const data = [ { label: "Red", value: "red" }, { label: "Green", value: "green" }, { label: "Blue", value: "blue" }, { label: "Yellow", value: "yellow" }, { label: "Purple", value: "purple" }, { label: "Orange", value: "orange" }, ] const collection = listbox.gridCollection({ items: data, columnCount: 3, }) const service = useMachine(listbox.machine, { id: useId(), collection, }) const api = computed(() => listbox.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Select color</label> <div v-bind="api.getContentProps()" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px" > <div v-for="item in collection.items" :key="item.value" v-bind="api.getItemProps({ item })" > <span v-bind="api.getItemTextProps({ item })">{{ item.label }}</span> <span v-bind="api.getItemIndicatorProps({ item })">✓</span> </div> </div> </div> </template>
<script lang="ts"> import * as listbox from "@zag-js/listbox" import { normalizeProps, useMachine } from "@zag-js/svelte" import { createUniqueId } from "@zag-js/utils" const data = [ { label: "Red", value: "red" }, { label: "Green", value: "green" }, { label: "Blue", value: "blue" }, { label: "Yellow", value: "yellow" }, { label: "Purple", value: "purple" }, { label: "Orange", value: "orange" }, ] const collection = listbox.gridCollection({ items: data, columnCount: 3, }) const service = useMachine(listbox.machine, { id: createUniqueId(), collection, }) const api = $derived(listbox.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Select color</label> <div {...api.getContentProps()} style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px" > {#each data as item} <div {...api.getItemProps({ item })}> <span {...api.getItemTextProps({ item })}>{item.label}</span> <span {...api.getItemIndicatorProps({ item })}>✓</span> </div> {/each} </div> </div>
Styling guide
Earlier, we mentioned that each listbox part has a data-part attribute added
to them to select and style them in the DOM.
[data-scope="listbox"][data-part="root"] { /* styles for the root part */ } [data-scope="listbox"][data-part="label"] { /* styles for the label part */ } [data-scope="listbox"][data-part="content"] { /* styles for the content part */ } [data-scope="listbox"][data-part="item"] { /* styles for the item part */ } [data-scope="listbox"][data-part="itemGroup"] { /* styles for the item group part */ }
Focused state
The focused state is applied to the item that is currently focused.
[data-scope="listbox"][data-part="item"][data-focused] { /* styles for the focused item part */ }
Selected state
The selected state is applied to the item that is currently selected.
[data-scope="listbox"][data-part="item"][data-selected] { /* styles for the selected item part */ }
Disabled state
The disabled state is applied to the item that is currently disabled.
[data-scope="listbox"][data-part="item"][data-disabled] { /* styles for the disabled item part */ }
Methods and Properties
Machine Context
The listbox machine exposes the following context properties:
collectionGridCollection<T>The item collectionidsPartial<{ root: string; content: string; label: string; item: (id: string | number) => string; itemGroup: (id: string | number) => string; itemGroupLabel: (id: string | number) => string; }>The ids of the elements in the listbox. Useful for composition.disabledbooleanWhether the listbox is disableddisallowSelectAllbooleanWhether to disallow selecting all items when `meta+a` is pressedonHighlightChange(details: HighlightChangeDetails<T>) => voidThe callback fired when the highlighted item changes.onValueChange(details: ValueChangeDetails<T>) => voidThe callback fired when the selected item changes.valuestring[]The controlled keys of the selected itemsdefaultValuestring[]The initial default value of the listbox when rendered. Use when you don't need to control the value of the listbox.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 listbox.loopFocusbooleanWhether to loop the keyboard navigation through the optionsselectionModeSelectionModeHow multiple selection should behave in the listbox. - `single`: The user can select a single item. - `multiple`: The user can select multiple items without using modifier keys. - `extended`: The user can select multiple items by using modifier keys.scrollToIndexFn(details: ScrollToIndexDetails) => voidFunction to scroll to a specific indexselectOnHighlightbooleanWhether to select the item when it is highlighteddeselectablebooleanWhether to disallow empty selectiontypeaheadbooleanWhether to enable typeahead on the listboxonSelect(details: SelectionDetails) => voidFunction called when an item is selecteddir"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.orientation"horizontal" | "vertical"The orientation of the element.
Machine API
The listbox api exposes the following methods:
emptybooleanWhether the select value is emptyhighlightedValuestringThe value of the highlighted itemhighlightedItemVThe highlighted itemhighlightValue(value: string) => voidFunction to highlight a valueclearHighlightedValueVoidFunctionFunction 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 values. **Note**: This should only be called when the selectionMode is `multiple` or `extended`. Otherwise, an exception will be thrown.setValue(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.getItemState(props: ItemProps<any>) => ItemStateReturns the state of a select itemcollectionListCollection<V>Function to toggle the selectdisabledbooleanWhether the select is disabled
Data Attributes
Accessibility
Adheres to the Listbox WAI-ARIA design pattern.
Edit this page on GitHub