Skip to main content

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

Loading...

Features

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

Installation

Install the cascade select package:

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

Anatomy

Check the cascade select anatomy and part names.

Each part includes a data-part attribute to help identify them in the DOM.

No anatomy available for cascade-select

Usage

Import the cascade select package:

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

These are the key exports:

  • machine - State machine logic.
  • connect - Maps machine state to JSX props and event handlers.
  • collection - Creates a tree collection from your hierarchical data.

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

Pass a unique id to useMachine so generated element ids stay predictable.

Then use the framework integration helpers:

<!-- CascadeSelectNode.vue --> <script setup lang="ts"> import { computed } from "vue" const props = defineProps(["node", "indexPath", "value", "api", "collection"]) const nodeProps = computed(() => ({ indexPath: props.indexPath, value: props.value, item: props.node, })) const nodeState = computed(() => props.api.getItemState(nodeProps.value)) const children = computed(() => props.collection.getNodeChildren(props.node)) </script> <template> <ul v-bind="api.getListProps(nodeProps)"> <li v-for="(item, index) in children" :key="collection.getNodeValue(item)" v-bind="api.getItemProps({ indexPath: [...indexPath, index], value: [...value, collection.getNodeValue(item)], item })" > <span v-bind="api.getItemTextProps({ indexPath: [...indexPath, index], value: [...value, collection.getNodeValue(item)], item })"> {{ item.label }} </span> <span></span> <span v-bind="api.getItemIndicatorProps({ indexPath: [...indexPath, index], value: [...value, collection.getNodeValue(item)], item })"></span> </li> </ul> <CascadeSelectNode v-if="nodeState.highlightedChild && collection.isBranchNode(nodeState.highlightedChild)" :node="nodeState.highlightedChild" :api="api" :collection="collection" :index-path="[...indexPath, nodeState.highlightedIndex]" :value="[...value, collection.getNodeValue(nodeState.highlightedChild)]" /> </template>
<!-- CascadeSelect.vue --> <script setup lang="ts"> import * as cascadeSelect from "@zag-js/cascade-select" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Teleport } from "vue" // 1. Create the collection (see above) const collection = cascadeSelect.collection<Node>({ // ... }) const service = useMachine(cascadeSelect.machine, { id: "1", collection, }) const api = computed(() => cascadeSelect.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Location</label> <div v-bind="api.getControlProps()"> <button v-bind="api.getTriggerProps()"> <span>{{ api.valueAsString || "Select location" }}</span> <span v-bind="api.getIndicatorProps()"></span> </button> <button v-bind="api.getClearTriggerProps()"></button> </div> <Teleport to="body"> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <CascadeSelectNode :node="collection.rootNode" :api="api" :collection="collection" :index-path="[]" :value="[]" /> </div> </div> </Teleport> </div> </template>

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"]], })

Controlled selection

Use value and onValueChange for controlled selection state.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, value, onValueChange(details) { setValue(details.value) }, })

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

By default, the menu closes when you select an item. Set closeOnSelect to false to keep it open.

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

Looping the keyboard navigation

By default, arrow-key navigation stops at the first and last items. Set loopFocus: true to loop back around.

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 => { highlightedValue: string[], highlightedItems: Item[] } console.log(details) }, })

Setting the initial highlighted path

Use defaultHighlightedValue to set the initially highlighted path.

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

Controlled highlighted path

Use highlightedValue and onHighlightChange to control highlighting externally.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, highlightedValue, onHighlightChange(details) { setHighlightedValue(details.highlightedValue) }, })

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

Use onOpenChange to listen for open and close events.

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

Controlling open state

Use open and onOpenChange for controlled open state, or defaultOpen for an uncontrolled initial state.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, open, onOpenChange({ open }) { setOpen(open) }, })
const service = useMachine(cascadeSelect.machine, { id: useId(), collection, defaultOpen: true, })

Positioning submenu panels

Use positioning to configure placement and collision behavior.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, positioning: { placement: "right-start", gutter: 4, }, })

Custom scroll behavior

Use scrollToIndexFn to customize how each level scrolls highlighted items into view.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, scrollToIndexFn(details) { // details => { index, depth, immediate? } customScroll(details) }, })

Customizing the trigger label

Use formatValue to control how selected paths are rendered in the trigger.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, formatValue(selectedItems) { return selectedItems .map((path) => path.map((item) => item.label).join(" / ")) .join(", ") }, })

Usage within a form

To use cascade select in a form, pass name. A hidden input is rendered with getHiddenInputProps().

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

If the hidden input belongs to a different form element, also set form.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, name: "location", form: "checkout-form", })

Validation and read-only state

Use readOnly, required, and invalid to control interaction and form state.

const service = useMachine(cascadeSelect.machine, { id: useId(), collection, readOnly: true, required: true, invalid: false, })

Styling guide

Each cascade select part includes a data-part attribute you can target in CSS.

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

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-focus
Present when focused
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-focus
Present when focused
data-placement
The placement of the trigger
data-placeholder-shown
Present when placeholder is shown
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
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"
ValueText
data-scope
cascade-select
data-part
value-text
data-disabled
Present when disabled
data-invalid
Present when invalid
data-focus
Present when focused

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