Listbox
A listbox displays selectable options in single or multiple selection modes.
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
Install the listbox package:
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
Check the listbox anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the listbox package:
import * as listbox from "@zag-js/listbox"
The listbox package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
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
Use value and onValueChange to control selection externally.
const service = useMachine(listbox.machine, { value: ["item-1", "item-2"], onValueChange(details) { // details => { value: string[]; items: CollectionItem[] } console.log(details.value) }, })
Controlling the highlighted item
Use highlightedValue and onHighlightChange to control highlighted state.
const service = useMachine(listbox.machine, { highlightedValue, onHighlightChange(details) { // details => { highlightedValue: string | null, highlightedItem, highlightedIndex } setHighlightedValue(details.highlightedValue) }, })
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
Use selectionMode to control selection behavior:
- 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", })
Selecting on highlight
Set selectOnHighlight to true to select items as they become highlighted.
const service = useMachine(listbox.machine, { selectOnHighlight: true, })
Disallowing select-all shortcuts
Set disallowSelectAll to disable Cmd/Ctrl + A selection.
const service = useMachine(listbox.machine, { selectionMode: "multiple", disallowSelectAll: true, })
Listening for item selection
Use onSelect to react whenever an item is selected.
const service = useMachine(listbox.machine, { onSelect(details) { // details => { value: string } console.log(details.value) }, })
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>
Horizontal orientation
Set orientation to horizontal for horizontal keyboard navigation.
const service = useMachine(listbox.machine, { orientation: "horizontal", collection, })
Styling guide
Each part includes a data-part attribute you can target in CSS.
[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:
orientation"horizontal" | "vertical"The orientation of the listbox.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.
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
CSS Variables
Accessibility
Adheres to the Listbox WAI-ARIA design pattern.
Edit this page on GitHub