{"slug":"cascade-select","title":"Cascade Select","description":"Using the cascade select machine in your project.","contentType":"component","framework":"react","content":"A Cascade Select component allows users to select from hierarchical data through\nmultiple linked levels of dropdown menus.\n\n## Resources\n\n\n[Latest version: v1.34.1](https://www.npmjs.com/package/@zag-js/cascade-select)\n[Logic Visualizer](https://zag-visualizer.vercel.app/cascade-select)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/cascade-select)\n\n\n\n**Features**\n\n- Support for hierarchical data with unlimited depth levels\n- Full keyboard navigation across all levels with arrow keys\n- Support for single and multiple selections\n- Support for both click and hover triggering modes\n- Support for looping keyboard navigation\n- Built-in accessibility with ARIA roles and keyboard interactions\n- Support for disabled items and read-only state\n- Form integration with hidden input element\n- Support for Right to Left direction.\n\n## Installation\n\nTo use the cascade select machine in your project, run the following command in\nyour command line:\n\n```bash\nnpm install @zag-js/cascade-select @zag-js/react\n# or\nyarn add @zag-js/cascade-select @zag-js/react\n```\n\n## Anatomy\n\nTo set up the cascade select correctly, you'll need to understand its anatomy\nand how we name its parts.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nFirst, import the cascade select package into your project\n\n```jsx\nimport * as cascadeSelect from \"@zag-js/cascade-select\"\n```\n\nThe cascade select package exports these functions:\n\n- `machine` — The state machine logic for the cascade select.\n- `connect` — The function that translates the machine's state to JSX\n  attributes and event handlers.\n- `collection` - The function that creates a tree collection from a tree node.\n\n### Create the collection\n\nUse the `collection` function to create a tree collection from your hierarchical\ndata. Pass a `rootNode` along with functions to extract each node's value,\nstring label, and children.\n\n```ts\nimport * as cascadeSelect from \"@zag-js/cascade-select\"\n\ninterface Node {\n  label: string\n  value: string\n  children?: Node[]\n}\n\nconst collection = cascadeSelect.collection<Node>({\n  nodeToValue: (node) => node.value,\n  nodeToString: (node) => node.label,\n  nodeToChildren: (node) => node.children,\n  rootNode: {\n    label: \"ROOT\",\n    value: \"ROOT\",\n    children: [\n      {\n        label: \"North America\",\n        value: \"north-america\",\n        children: [\n          {\n            label: \"United States\",\n            value: \"us\",\n            children: [\n              { label: \"New York\", value: \"ny\" },\n              { label: \"California\", value: \"ca\" },\n            ],\n          },\n          { label: \"Canada\", value: \"canada\" },\n        ],\n      },\n      {\n        label: \"Africa\",\n        value: \"africa\",\n        children: [\n          { label: \"Nigeria\", value: \"ng\" },\n          { label: \"Kenya\", value: \"ke\" },\n        ],\n      },\n    ],\n  },\n})\n```\n\n### Create the cascade select\n\n> You'll need to provide a unique `id` to the `useMachine` hook. This is used\n> to ensure that every part has a unique identifier.\n\nPass the collection to the machine to create the cascade select 🔥\n\n```tsx\nimport * as cascadeSelect from \"@zag-js/cascade-select\"\nimport { normalizeProps, Portal, useMachine } from \"@zag-js/react\"\nimport { JSX, useId } from \"react\"\n\n// 1. Create the collection (see above)\nconst collection = cascadeSelect.collection<Node>({\n  // ...\n})\n\n// 2. Create the recursive tree node\n\ninterface TreeNodeProps {\n  node: Node\n  indexPath?: number[]\n  value?: string[]\n  api: cascadeSelect.Api\n}\n\nconst TreeNode = (props: TreeNodeProps): JSX.Element => {\n  const { node, indexPath = [], value = [], api } = props\n\n  const nodeProps = { indexPath, value, item: node }\n  const nodeState = api.getItemState(nodeProps)\n  const children = collection.getNodeChildren(node)\n\n  return (\n    <>\n      <ul {...api.getListProps(nodeProps)}>\n        {children.map((item, index) => {\n          const itemProps = {\n            indexPath: [...indexPath, index],\n            value: [...value, collection.getNodeValue(item)],\n            item,\n          }\n          const itemState = api.getItemState(itemProps)\n          return (\n            <li key={collection.getNodeValue(item)} {...api.getItemProps(itemProps)}>\n              <span {...api.getItemTextProps(itemProps)}>{item.label}</span>\n              {itemState.hasChildren && <span>›</span>}\n              <span {...api.getItemIndicatorProps(itemProps)}>✓</span>\n            </li>\n          )\n        })}\n      </ul>\n      {nodeState.highlightedChild &&\n        collection.isBranchNode(nodeState.highlightedChild) && (\n          <TreeNode\n            node={nodeState.highlightedChild}\n            api={api}\n            indexPath={[...indexPath, nodeState.highlightedIndex]}\n            value={[...value, collection.getNodeValue(nodeState.highlightedChild)]}\n          />\n        )}\n    </>\n  )\n}\n\n// 3. Create the cascade select\n\nexport function CascadeSelect() {\n  const service = useMachine(cascadeSelect.machine, {\n    id: useId(),\n    collection,\n  })\n\n  const api = cascadeSelect.connect(service, normalizeProps)\n\n  return (\n    <div {...api.getRootProps()}>\n      <label {...api.getLabelProps()}>Location</label>\n      <div {...api.getControlProps()}>\n        <button {...api.getTriggerProps()}>\n          <span>{api.valueAsString || \"Select location\"}</span>\n          <span {...api.getIndicatorProps()}>▼</span>\n        </button>\n        <button {...api.getClearTriggerProps()}>✕</button>\n      </div>\n      <Portal>\n        <div {...api.getPositionerProps()}>\n          <div {...api.getContentProps()}>\n            <TreeNode node={collection.rootNode} api={api} />\n          </div>\n        </div>\n      </Portal>\n    </div>\n  )\n}\n```\n\n### Setting the initial value\n\nUse the `defaultValue` property to set the initial value of the cascade select.\n\n> The `value` property must be an array of string paths. Each path is an array\n> of values from the root to the selected leaf item.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  defaultValue: [[\"north-america\", \"us\", \"ny\"]],\n})\n```\n\n### Selecting multiple values\n\nTo allow selecting multiple values, set the `multiple` property to `true`.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  multiple: true,\n})\n```\n\n### Hover triggering\n\nBy default, items are highlighted when clicked. To highlight items on hover\ninstead (like a traditional cascading menu), set the `highlightTrigger` property\nto `\"hover\"`.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  highlightTrigger: \"hover\",\n})\n```\n\n### Allowing parent selection\n\nBy default, only leaf nodes (items without children) can be selected. To allow\nparent (branch) nodes to also be selectable, set `allowParentSelection` to\n`true`.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  allowParentSelection: true,\n})\n```\n\n### Close on select\n\nThis behaviour ensures that the menu is closed when an item is selected and is\n`true` by default. To keep the dropdown open after selection, set the\n`closeOnSelect` property to `false`.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  closeOnSelect: false,\n})\n```\n\n### Looping the keyboard navigation\n\nWhen navigating with the cascade select using the arrow down and up keys, the\nnavigation stops at the first and last items. To loop navigation back to the\nfirst or last item, set `loopFocus: true` in the machine's context.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  loopFocus: true,\n})\n```\n\n### Listening for highlight changes\n\nWhen an item is highlighted with the pointer or keyboard, use the\n`onHighlightChange` to listen for the change and do something with it.\n\n```jsx {3-6}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  onHighlightChange(details) {\n    // details => { value: string[], items: Item[] }\n    console.log(details)\n  },\n})\n```\n\n### Listening for selection changes\n\nWhen an item is selected, use the `onValueChange` property to listen for the\nchange and do something with it.\n\n```jsx {4-7}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  onValueChange(details) {\n    // details => { value: string[][], items: Item[][] }\n    console.log(details)\n  },\n})\n```\n\n### Listening for open and close events\n\nWhen the cascade select is opened or closed, the `onOpenChange` callback is\ncalled. You can listen for these events and do something with it.\n\n```jsx {4-7}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  onOpenChange(details) {\n    // details => { open: boolean }\n    console.log(details)\n  },\n})\n```\n\n### Usage within a form\n\nTo use cascade select within a form, pass the `name` property to the machine\ncontext. A hidden input element is automatically rendered using\n`getHiddenInputProps()`.\n\n```jsx {4}\nconst service = useMachine(cascadeSelect.machine, {\n  id: useId(),\n  collection,\n  name: \"location\",\n})\n\n// In your JSX\n<input {...api.getHiddenInputProps()} />\n```\n\n## Styling guide\n\nEarlier, we mentioned that each cascade select part has a `data-part` attribute\nadded to them to select and style them in the DOM.\n\n### Open and closed state\n\nWhen the cascade select is open, the trigger and content is given a `data-state`\nattribute.\n\n```css\n[data-part=\"trigger\"][data-state=\"open|closed\"] {\n  /* styles for open or closed state */\n}\n\n[data-part=\"content\"][data-state=\"open|closed\"] {\n  /* styles for open or closed state */\n}\n```\n\n### Selected state\n\nItems are given a `data-state` attribute, indicating whether they are selected.\n\n```css\n[data-part=\"item\"][data-state=\"checked|unchecked\"] {\n  /* styles for selected or unselected state */\n}\n```\n\n### Highlighted state\n\nWhen an item is highlighted, via keyboard navigation or pointer, it is given a\n`data-highlighted` attribute.\n\n```css\n[data-part=\"item\"][data-highlighted] {\n  /* styles for highlighted state */\n}\n```\n\n### Branch items\n\nWhen an item has children (is a branch node), it is given a `data-has-children`\nattribute.\n\n```css\n[data-part=\"item\"][data-has-children] {\n  /* styles for items with children (branch nodes) */\n}\n```\n\n### Invalid state\n\nWhen the cascade select is invalid, the label and trigger is given a\n`data-invalid` attribute.\n\n```css\n[data-part=\"label\"][data-invalid] {\n  /* styles for invalid state */\n}\n\n[data-part=\"trigger\"][data-invalid] {\n  /* styles for invalid state */\n}\n```\n\n### Disabled state\n\nWhen the cascade select is disabled, the trigger and label is given a\n`data-disabled` attribute.\n\n```css\n[data-part=\"trigger\"][data-disabled] {\n  /* styles for disabled state */\n}\n\n[data-part=\"label\"][data-disabled] {\n  /* styles for disabled label state */\n}\n\n[data-part=\"item\"][data-disabled] {\n  /* styles for disabled item state */\n}\n```\n\n### Empty state\n\nWhen no option is selected, the trigger is given a `data-placeholder-shown`\nattribute.\n\n```css\n[data-part=\"trigger\"][data-placeholder-shown] {\n  /* styles for empty state */\n}\n```\n\n## Methods and Properties\n\n### Machine Context\n\nThe cascade select machine exposes the following context properties:\n\n**`collection`**\nType: `TreeCollection<T>`\nDescription: The tree collection data\n\n**`ids`**\nType: `Partial<{ 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; }>`\nDescription: The ids of the cascade-select elements. Useful for composition.\n\n**`name`**\nType: `string`\nDescription: The name attribute of the underlying input element\n\n**`form`**\nType: `string`\nDescription: The form attribute of the underlying input element\n\n**`value`**\nType: `string[][]`\nDescription: The controlled value of the cascade-select\n\n**`defaultValue`**\nType: `string[][]`\nDescription: The initial value of the cascade-select when rendered.\nUse when you don't need to control the value.\n\n**`highlightedValue`**\nType: `string[]`\nDescription: The controlled highlighted value of the cascade-select\n\n**`defaultHighlightedValue`**\nType: `string[]`\nDescription: The initial highlighted value of the cascade-select when rendered.\n\n**`multiple`**\nType: `boolean`\nDescription: Whether to allow multiple selections\n\n**`open`**\nType: `boolean`\nDescription: The controlled open state of the cascade-select\n\n**`defaultOpen`**\nType: `boolean`\nDescription: The initial open state of the cascade-select when rendered.\nUse when you don't need to control the open state.\n\n**`highlightTrigger`**\nType: `\"click\" | \"hover\"`\nDescription: What triggers highlighting of items\n\n**`closeOnSelect`**\nType: `boolean`\nDescription: Whether the cascade-select should close when an item is selected\n\n**`loopFocus`**\nType: `boolean`\nDescription: Whether the cascade-select should loop focus when navigating with keyboard\n\n**`disabled`**\nType: `boolean`\nDescription: Whether the cascade-select is disabled\n\n**`readOnly`**\nType: `boolean`\nDescription: Whether the cascade-select is read-only\n\n**`required`**\nType: `boolean`\nDescription: Whether the cascade-select is required\n\n**`invalid`**\nType: `boolean`\nDescription: Whether the cascade-select is invalid\n\n**`positioning`**\nType: `PositioningOptions`\nDescription: The positioning options for the cascade-select content\n\n**`scrollToIndexFn`**\nType: `(details: ScrollToIndexDetails) => void`\nDescription: Function to scroll to a specific index in a list\n\n**`formatValue`**\nType: `(selectedItems: T[][]) => string`\nDescription: Function to format the display value\n\n**`onValueChange`**\nType: `(details: ValueChangeDetails<T>) => void`\nDescription: Called when the value changes\n\n**`onHighlightChange`**\nType: `(details: HighlightChangeDetails<T>) => void`\nDescription: Called when the highlighted value changes\n\n**`onOpenChange`**\nType: `(details: OpenChangeDetails) => void`\nDescription: Called when the open state changes\n\n**`allowParentSelection`**\nType: `boolean`\nDescription: Whether parent (branch) items can be selectable\n\n**`dir`**\nType: `\"ltr\" | \"rtl\"`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `() => ShadowRoot | Node | Document`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n**`onPointerDownOutside`**\nType: `(event: PointerDownOutsideEvent) => void`\nDescription: Function called when the pointer is pressed down outside the component\n\n**`onFocusOutside`**\nType: `(event: FocusOutsideEvent) => void`\nDescription: Function called when the focus is moved outside the component\n\n**`onInteractOutside`**\nType: `(event: InteractOutsideEvent) => void`\nDescription: Function called when an interaction happens outside the component\n\n### Machine API\n\nThe cascade select `api` exposes the following methods:\n\n**`collection`**\nType: `TreeCollection<V>`\nDescription: The tree collection data\n\n**`open`**\nType: `boolean`\nDescription: Whether the cascade-select is open\n\n**`focused`**\nType: `boolean`\nDescription: Whether the cascade-select is focused\n\n**`multiple`**\nType: `boolean`\nDescription: Whether the cascade-select allows multiple selections\n\n**`disabled`**\nType: `boolean`\nDescription: Whether the cascade-select is disabled\n\n**`highlightedValue`**\nType: `string[]`\nDescription: The value of the highlighted item\n\n**`highlightedItems`**\nType: `V[]`\nDescription: The items along the highlighted path\n\n**`selectedItems`**\nType: `V[][]`\nDescription: The selected items\n\n**`hasSelectedItems`**\nType: `boolean`\nDescription: Whether there's a selected option\n\n**`empty`**\nType: `boolean`\nDescription: Whether the cascade-select value is empty\n\n**`value`**\nType: `string[][]`\nDescription: The current value of the cascade-select\n\n**`valueAsString`**\nType: `string`\nDescription: The current value as text\n\n**`focus`**\nType: `() => void`\nDescription: Function to focus on the select input\n\n**`reposition`**\nType: `(options?: Partial<PositioningOptions>) => void`\nDescription: Function to set the positioning options of the cascade-select\n\n**`setOpen`**\nType: `(open: boolean) => void`\nDescription: Function to open the cascade-select\n\n**`setHighlightValue`**\nType: `(value: string | string[]) => void`\nDescription: Function to set the highlighted value (path or single value to find)\n\n**`clearHighlightValue`**\nType: `() => void`\nDescription: Function to clear the highlighted value\n\n**`selectValue`**\nType: `(value: string[]) => void`\nDescription: Function to select a value\n\n**`setValue`**\nType: `(value: string[][]) => void`\nDescription: Function to set the value\n\n**`clearValue`**\nType: `(value?: string[]) => void`\nDescription: Function to clear the value\n\n**`getItemState`**\nType: `(props: ItemProps<V>) => ItemState<V>`\nDescription: Returns the state of a cascade-select item\n\n**`getValueTextProps`**\nType: `() => T[\"element\"]`\nDescription: Returns the props for the value text element\n\n### Data Attributes\n\n**`Root`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: root\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n**`data-state`**: \"open\" | \"closed\"\n\n**`Label`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: label\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n\n**`Control`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: control\n**`data-disabled`**: Present when disabled\n**`data-focused`**: \n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n**`data-state`**: \"open\" | \"closed\"\n\n**`Trigger`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: trigger\n**`data-state`**: \"open\" | \"closed\"\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n**`data-focused`**: \n**`data-placement`**: The placement of the trigger\n\n**`ClearTrigger`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: clear-trigger\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n\n**`Content`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: content\n**`data-activedescendant`**: The id the active descendant of the content\n**`data-state`**: \"open\" | \"closed\"\n\n**`List`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: list\n**`data-depth`**: The depth of the item\n\n**`Indicator`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: indicator\n**`data-state`**: \"open\" | \"closed\"\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n\n**`ValueText`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: value-text\n**`data-disabled`**: Present when disabled\n**`data-invalid`**: Present when invalid\n**`data-focused`**: Present when focused\n\n**`Item`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: item\n**`data-value`**: The value of the item\n**`data-disabled`**: Present when disabled\n**`data-highlighted`**: Present when highlighted\n**`data-selected`**: Present when selected\n**`data-depth`**: The depth of the item\n**`data-state`**: \"checked\" | \"unchecked\"\n**`data-type`**: The type of the item\n**`data-index-path`**: \n\n**`ItemText`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: item-text\n**`data-value`**: The value of the item\n**`data-highlighted`**: Present when highlighted\n**`data-state`**: \"checked\" | \"unchecked\"\n**`data-disabled`**: Present when disabled\n\n**`ItemIndicator`**\n\n**`data-scope`**: cascade-select\n**`data-part`**: item-indicator\n**`data-value`**: The value of the item\n**`data-highlighted`**: Present when highlighted\n**`data-type`**: The type of the item\n**`data-state`**: \"checked\" | \"unchecked\"\n\n### CSS Variables\n\n<CssVarTable name=\"cascade-select\" />\n\n## Accessibility\n\nAdheres to the\n[ListBox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox).\n\n### Keyboard Interactions\n\n**`Space`**\nDescription: <span>When focus is on trigger, opens the cascade select and focuses the first item.<br />When focus is on the content, selects the highlighted item.</span>\n\n**`Enter`**\nDescription: <span>When focus is on trigger, opens the cascade select and focuses the first item.<br />When focus is on content, selects the highlighted item.</span>\n\n**`ArrowDown`**\nDescription: <span>When focus is on trigger, opens the cascade select.<br />When focus is on content, moves focus to the next item in the current level.</span>\n\n**`ArrowUp`**\nDescription: <span>When focus is on trigger, opens the cascade select and focuses the last item.<br />When focus is on content, moves focus to the previous item in the current level.</span>\n\n**`ArrowRight`**\nDescription: <span>When focus is on a branch item, expands the next level and moves focus into it.</span>\n\n**`ArrowLeft`**\nDescription: <span>When focus is on a nested level, collapses it and moves focus back to the parent.<br />When focus is at the root level, closes the cascade select.</span>\n\n**`Home`**\nDescription: <span>Moves focus to the first item in the current level.</span>\n\n**`End`**\nDescription: <span>Moves focus to the last item in the current level.</span>\n\n**`Esc`**\nDescription: <span>Closes the cascade select and moves focus to trigger.</span>","package":"@zag-js/cascade-select","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/cascade-select.mdx"}