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 { normalizeProps, useMachine } 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 { normalizeProps, useMachine } 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" | undefinedThe orientation of the listbox.collectionanyThe item collectionidsPartial<{ root: string; content: string; label: string; item: (id: string | number) => string; itemGroup: (id: string | number) => string; itemGroupLabel: (id: string | number) => string; }> | undefinedThe ids of the elements in the listbox. Useful for composition.disabledboolean | undefinedWhether the listbox is disableddisallowSelectAllboolean | undefinedWhether to disallow selecting all items when `meta+a` is pressedonHighlightChange((details: HighlightChangeDetails<T>) => void) | undefinedThe callback fired when the highlighted item changes.onValueChange((details: ValueChangeDetails<T>) => void) | undefinedThe callback fired when the selected item changes.valuestring[] | undefinedThe controlled keys of the selected itemsdefaultValuestring[] | undefinedThe initial default value of the listbox when rendered. Use when you don't need to control the value of the listbox.highlightedValuestring | null | undefinedThe controlled key of the highlighted itemdefaultHighlightedValuestring | null | undefinedThe initial value of the highlighted item when opened. Use when you don't need to control the highlighted value of the listbox.loopFocusboolean | undefinedWhether to loop the keyboard navigation through the optionsselectionModeanyHow 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) => void) | undefinedFunction to scroll to a specific indexselectOnHighlightboolean | undefinedWhether to select the item when it is highlighteddeselectableboolean | undefinedWhether to disallow empty selectiontypeaheadboolean | undefinedWhether to enable typeahead on the listboxonSelect((details: SelectionDetails) => void) | undefinedFunction called when an item is selecteddir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA 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 emptyhighlightedValuestring | nullThe value of the highlighted itemhighlightedItemV | nullThe highlighted itemhighlightValue(value: string) => voidFunction to highlight a valuehighlightFirstVoidFunctionFunction to highlight the first valuehighlightLastVoidFunctionFunction to highlight the last valuehighlightNextVoidFunctionFunction to highlight the next valuehighlightPreviousVoidFunctionFunction to highlight the previous 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 | undefined) => 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<CollectionItem>) => 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