Skip to main content

A Cascade Select component allows users to select from hierarchical data through multiple linked levels of dropdown menus.

Loading...

Features

  • Support for hierarchical data with unlimited depth levels
  • Full keyboard navigation across all levels with arrow keys
  • Support for single and multiple selections
  • Support for both click and hover triggering modes
  • Support for looping keyboard navigation
  • Built-in accessibility with ARIA roles and keyboard interactions
  • Support for disabled items and read-only state
  • Form integration with hidden input element
  • Support for Right to Left direction.

Installation

To use the cascade select machine in your project, run the following command in your command line:

npm install @zag-js/cascade-select @zag-js/svelte # or yarn add @zag-js/cascade-select @zag-js/svelte

Anatomy

To set up the cascade 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.

No anatomy available for cascade-select

Usage

First, import the cascade select package into your project

import * as cascadeSelect from "@zag-js/cascade-select"

The cascade select package exports these functions:

  • machine — The state machine logic for the cascade select.
  • connect — The function that translates the machine's state to JSX attributes and event handlers.
  • collection - The function that creates a tree collection from a tree node.

Create the collection

Use the collection function to create a tree collection from your hierarchical data. Pass a rootNode along with functions to extract each node's value, string label, and children.

import * as cascadeSelect from "@zag-js/cascade-select" interface Node { label: string value: string children?: Node[] } const collection = cascadeSelect.collection<Node>({ nodeToValue: (node) => node.value, nodeToString: (node) => node.label, nodeToChildren: (node) => node.children, rootNode: { label: "ROOT", value: "ROOT", children: [ { label: "North America", value: "north-america", children: [ { label: "United States", value: "us", children: [ { label: "New York", value: "ny" }, { label: "California", value: "ca" }, ], }, { label: "Canada", value: "canada" }, ], }, { label: "Africa", value: "africa", children: [ { label: "Nigeria", value: "ng" }, { label: "Kenya", value: "ke" }, ], }, ], }, })

Create the cascade select

You'll need to provide a unique id to the useMachine hook. This is used to ensure that every part has a unique identifier.

Pass the collection to the machine to create the cascade select 🔥

<script lang="ts"> import * as cascadeSelect from "@zag-js/cascade-select" import { normalizeProps, portal, useMachine } from "@zag-js/svelte" // 1. Create the collection (see above) interface Node { label: string value: string children?: Node[] } const collection = cascadeSelect.collection<Node>({ // ... }) // 2. Create the recursive tree node const id = $props.id() const service = useMachine(cascadeSelect.machine, { id, collection }) const api = $derived(cascadeSelect.connect(service, normalizeProps)) </script> {#snippet treeNode(nodeProps)} {@const { node, indexPath, value, api } = nodeProps} {@const listNodeProps = { indexPath, value, item: node }} {@const nodeState = api.getItemState(listNodeProps)} {@const children = collection.getNodeChildren(node)} <ul {...api.getListProps(listNodeProps)}> {#each children as item, index} {@const itemProps = { indexPath: [...indexPath, index], value: [...value, collection.getNodeValue(item)], item, }} {@const itemState = api.getItemState(itemProps)} <li {...api.getItemProps(itemProps)}> <span {...api.getItemTextProps(itemProps)}>{item.label}</span> {#if itemState.hasChildren}<span></span>{/if} <span {...api.getItemIndicatorProps(itemProps)}></span> </li> {/each} </ul> {#if nodeState.highlightedChild && collection.isBranchNode(nodeState.highlightedChild)} {@render treeNode({ node: nodeState.highlightedChild, api, indexPath: [...indexPath, nodeState.highlightedIndex], value: [...value, collection.getNodeValue(nodeState.highlightedChild)], })} {/if} {/snippet} <!-- 3. Create the cascade select --> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Location</label> <div {...api.getControlProps()}> <button {...api.getTriggerProps()}> <span>{api.valueAsString || "Select location"}</span> <span {...api.getIndicatorProps()}></span> </button> <button {...api.getClearTriggerProps()}></button> </div> <div use:portal {...api.getPositionerProps()}> <div {...api.getContentProps()}> {@render treeNode({ node: collection.rootNode, api, indexPath: [], value: [] })} </div> </div> </div>

Setting the initial value

Use the defaultValue property to set the initial value of the cascade select.

The value property must be an array of string paths. Each path is an array of values from the root to the selected leaf item.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, defaultValue: [["north-america", "us", "ny"]], })

Selecting multiple values

To allow selecting multiple values, set the multiple property to true.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, multiple: true, })

Hover triggering

By default, items are highlighted when clicked. To highlight items on hover instead (like a traditional cascading menu), set the highlightTrigger property to "hover".

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, highlightTrigger: "hover", })

Allowing parent selection

By default, only leaf nodes (items without children) can be selected. To allow parent (branch) nodes to also be selectable, set allowParentSelection to true.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, allowParentSelection: true, })

Close on select

This behaviour ensures that the menu is closed when an item is selected and is true by default. To keep the dropdown open after selection, set the closeOnSelect property to false.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, closeOnSelect: false, })

Looping the keyboard navigation

When navigating with the cascade select using the arrow down and up keys, the navigation stops at the first and last items. To loop navigation back to the first or last item, set loopFocus: true in the machine's context.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, loopFocus: 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(cascadeSelect.machine, { id: useId(), onHighlightChange(details) { // details => { value: string[], items: Item[] } 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(cascadeSelect.machine, { id: useId(), collection, onValueChange(details) { // details => { value: string[][], items: Item[][] } console.log(details) }, })

Listening for open and close events

When the cascade select is opened or closed, the onOpenChange callback is called. You can listen for these events and do something with it.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, onOpenChange(details) { // details => { open: boolean } console.log(details) }, })

Usage within a form

To use cascade select within a form, pass the name property to the machine context. A hidden input element is automatically rendered using getHiddenInputProps().

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, name: "location", }) // In your JSX <input {...api.getHiddenInputProps()} />

Styling guide

Earlier, we mentioned that each cascade select part has a data-part attribute added to them to select and style them in the DOM.

Open and closed state

When the cascade 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 */ }

Branch items

When an item has children (is a branch node), it is given a data-has-children attribute.

[data-part="item"][data-has-children] { /* styles for items with children (branch nodes) */ }

Invalid state

When the cascade 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 cascade select is disabled, the trigger and label is given a data-disabled attribute.

[data-part="trigger"][data-disabled] { /* styles for disabled state */ } [data-part="label"][data-disabled] { /* styles for disabled label state */ } [data-part="item"][data-disabled] { /* styles for disabled item state */ }

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 state */ }

Methods and Properties

Machine Context

The cascade select machine exposes the following context properties:

  • collectionTreeCollection<T>The tree collection data
  • idsPartial<{ root: string; label: string; control: string; trigger: string; indicator: string; clearTrigger: string; positioner: string; content: string; hiddenInput: string; list(valuePath: string): string; item(valuePath: string): string; }>The ids of the cascade-select elements. Useful for composition.
  • namestringThe name attribute of the underlying input element
  • formstringThe form attribute of the underlying input element
  • valuestring[][]The controlled value of the cascade-select
  • defaultValuestring[][]The initial value of the cascade-select when rendered. Use when you don't need to control the value.
  • highlightedValuestring[]The controlled highlighted value of the cascade-select
  • defaultHighlightedValuestring[]The initial highlighted value of the cascade-select when rendered.
  • multiplebooleanWhether to allow multiple selections
  • openbooleanThe controlled open state of the cascade-select
  • defaultOpenbooleanThe initial open state of the cascade-select when rendered. Use when you don't need to control the open state.
  • highlightTrigger"click" | "hover"What triggers highlighting of items
  • closeOnSelectbooleanWhether the cascade-select should close when an item is selected
  • loopFocusbooleanWhether the cascade-select should loop focus when navigating with keyboard
  • disabledbooleanWhether the cascade-select is disabled
  • readOnlybooleanWhether the cascade-select is read-only
  • requiredbooleanWhether the cascade-select is required
  • invalidbooleanWhether the cascade-select is invalid
  • positioningPositioningOptionsThe positioning options for the cascade-select content
  • scrollToIndexFn(details: ScrollToIndexDetails) => voidFunction to scroll to a specific index in a list
  • formatValue(selectedItems: T[][]) => stringFunction to format the display value
  • onValueChange(details: ValueChangeDetails<T>) => voidCalled when the value changes
  • onHighlightChange(details: HighlightChangeDetails<T>) => voidCalled when the highlighted value changes
  • onOpenChange(details: OpenChangeDetails) => voidCalled when the open state changes
  • allowParentSelectionbooleanWhether parent (branch) items can be selectable
  • 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 cascade select api exposes the following methods:

  • collectionTreeCollection<V>The tree collection data
  • openbooleanWhether the cascade-select is open
  • focusedbooleanWhether the cascade-select is focused
  • multiplebooleanWhether the cascade-select allows multiple selections
  • disabledbooleanWhether the cascade-select is disabled
  • highlightedValuestring[]The value of the highlighted item
  • highlightedItemsV[]The items along the highlighted path
  • selectedItemsV[][]The selected items
  • hasSelectedItemsbooleanWhether there's a selected option
  • emptybooleanWhether the cascade-select value is empty
  • valuestring[][]The current value of the cascade-select
  • valueAsStringstringThe current value as text
  • focus() => voidFunction to focus on the select input
  • reposition(options?: Partial<PositioningOptions>) => voidFunction to set the positioning options of the cascade-select
  • setOpen(open: boolean) => voidFunction to open the cascade-select
  • setHighlightValue(value: string | string[]) => voidFunction to set the highlighted value (path or single value to find)
  • clearHighlightValue() => voidFunction to clear the highlighted value
  • selectValue(value: string[]) => voidFunction to select a value
  • setValue(value: string[][]) => voidFunction to set the value
  • clearValue(value?: string[]) => voidFunction to clear the value
  • getItemState(props: ItemProps<V>) => ItemState<V>Returns the state of a cascade-select item
  • getValueTextProps() => T["element"]Returns the props for the value text element

Data Attributes

Root
data-scope
cascade-select
data-part
root
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
data-state
"open" | "closed"
Label
data-scope
cascade-select
data-part
label
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
Control
data-scope
cascade-select
data-part
control
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
data-state
"open" | "closed"
Trigger
data-scope
cascade-select
data-part
trigger
data-state
"open" | "closed"
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
data-placement
The placement of the trigger
ClearTrigger
data-scope
cascade-select
data-part
clear-trigger
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
Content
data-scope
cascade-select
data-part
content
data-activedescendant
The id the active descendant of the content
data-state
"open" | "closed"
List
data-scope
cascade-select
data-part
list
data-depth
The depth of the item
Indicator
data-scope
cascade-select
data-part
indicator
data-state
"open" | "closed"
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
ValueText
data-scope
cascade-select
data-part
value-text
data-disabled
Present when disabled
data-invalid
Present when invalid
data-focused
Present when focused
Item
data-scope
cascade-select
data-part
item
data-value
The value of the item
data-disabled
Present when disabled
data-highlighted
Present when highlighted
data-selected
Present when selected
data-depth
The depth of the item
data-state
"checked" | "unchecked"
data-type
The type of the item
ItemText
data-scope
cascade-select
data-part
item-text
data-value
The value of the item
data-highlighted
Present when highlighted
data-state
"checked" | "unchecked"
data-disabled
Present when disabled
ItemIndicator
data-scope
cascade-select
data-part
item-indicator
data-value
The value of the item
data-highlighted
Present when highlighted
data-type
The type of the item
data-state
"checked" | "unchecked"

CSS Variables

Arrow
--arrow-size
The size of the arrow
--arrow-size-half
Half the size of the arrow
--arrow-background
Use this variable to style the arrow background
--arrow-offset
The offset position of the arrow
Positioner
--reference-width
The width of the reference element
--reference-height
The height of the root
--available-width
The available width in viewport
--available-height
The available height in viewport
--x
The x position for transform
--y
The y position for transform
--z-index
The z-index value
--transform-origin
The transform origin for animations
Content
--layer-index
The index of the dismissable in the layer stack
--nested-layer-count
The number of nested cascade-selects
Backdrop
--layer-index
The index of the dismissable in the layer stack

Accessibility

Adheres to the ListBox WAI-ARIA design pattern.

Keyboard Interactions

  • Space
    When focus is on trigger, opens the cascade select and focuses the first item.
    When focus is on the content, selects the highlighted item.
  • Enter
    When focus is on trigger, opens the cascade select and focuses the first item.
    When focus is on content, selects the highlighted item.
  • ArrowDown
    When focus is on trigger, opens the cascade select.
    When focus is on content, moves focus to the next item in the current level.
  • ArrowUp
    When focus is on trigger, opens the cascade select and focuses the last item.
    When focus is on content, moves focus to the previous item in the current level.
  • ArrowRight
    When focus is on a branch item, expands the next level and moves focus into it.
  • ArrowLeft
    When focus is on a nested level, collapses it and moves focus back to the parent.
    When focus is at the root level, closes the cascade select.
  • Home
    Moves focus to the first item in the current level.
  • End
    Moves focus to the last item in the current level.
  • Esc
    Closes the cascade select and moves focus to trigger.
Edit this page on GitHub