Tree View
The TreeView component provides a hierarchical view of data, similar to a file system explorer. It allows users to expand and collapse branches, select individual or multiple nodes, and traverse the hierarchy using keyboard navigation.
My Documents
Features
- Display hierarchical data in a tree structure
- Expand or collapse nodes
- Support for keyboard navigation
- Select single or multiple nodes (depending on the selection mode)
- Perform actions on the nodes, such as deleting them or performing some other operation
Installation
To use the tree view machine in your project, run the following command in your command line:
npm install @zag-js/tree-view @zag-js/react # or yarn add @zag-js/tree-view @zag-js/react
npm install @zag-js/tree-view @zag-js/solid # or yarn add @zag-js/tree-view @zag-js/solid
npm install @zag-js/tree-view @zag-js/vue # or yarn add @zag-js/tree-view @zag-js/vue
npm install @zag-js/tree-view @zag-js/svelte # or yarn add @zag-js/tree-view @zag-js/svelte
Anatomy
To set up the tree view correctly, you'll need to understand its anatomy.
Usage
First, import the tree view package into your project
import * as tree from "@zag-js/tree-view"
The tree view package exports two key functions:
machine— The state machine logic for the tree view widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
Next, import the required hooks and functions for your framework and use the tree view machine in your project 🔥
Create the tree collection
Use the collection function to create a tree collection. This create a tree
factory that the component uses for traversal.
import * as tree from "@zag-js/tree-view" interface Node { id: string name: string children?: Node[] } const collection = tree.collection<Node>({ nodeToValue: (node) => node.id, nodeToString: (node) => node.name, rootNode: { id: "ROOT", name: "", children: [ { id: "node_modules", name: "node_modules", children: [ { id: "node_modules/zag-js", name: "zag-js" }, { id: "node_modules/pandacss", name: "panda" }, { id: "node_modules/@types", name: "@types", children: [ { id: "node_modules/@types/react", name: "react" }, { id: "node_modules/@types/react-dom", name: "react-dom" }, ], }, ], }, ], }, })
Create the tree view
Pass the tree collection to the machine to create the tree view.
import { normalizeProps, useMachine } from "@zag-js/react" import * as tree from "@zag-js/tree-view" import { FileIcon, FolderIcon, ChevronRightIcon } from "lucide-react" import { useId } from "react" // 1. Create the tree collection interface Node { id: string name: string children?: Node[] } const collection = tree.collection<Node>({ // ... }) // 2. Create the recursive tree node interface TreeNodeProps { node: Node indexPath: number[] api: tree.Api } const TreeNode = (props: TreeNodeProps): JSX.Element => { const { node, indexPath, api } = props const nodeProps = { indexPath, node } const nodeState = api.getNodeState(nodeProps) if (nodeState.isBranch) { return ( <div {...api.getBranchProps(nodeProps)}> <div {...api.getBranchControlProps(nodeProps)}> <FolderIcon /> <span {...api.getBranchTextProps(nodeProps)}>{node.name}</span> <span {...api.getBranchIndicatorProps(nodeProps)}> <ChevronRightIcon /> </span> </div> <div {...api.getBranchContentProps(nodeProps)}> <div {...api.getBranchIndentGuideProps(nodeProps)} /> {node.children?.map((childNode, index) => ( <TreeNode key={childNode.id} node={childNode} indexPath={[...indexPath, index]} api={api} /> ))} </div> </div> ) } return ( <div {...api.getItemProps(nodeProps)}> <FileIcon /> {node.name} </div> ) } // 3. Create the tree view export function TreeView() { const service = useMachine(tree.machine, { id: useId(), collection }) const api = tree.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <h3 {...api.getLabelProps()}>My Documents</h3> <div {...api.getTreeProps()}> {collection.rootNode.children?.map((node, index) => ( <TreeNode key={node.id} node={node} indexPath={[index]} api={api} /> ))} </div> </div> ) }
import { normalizeProps, useMachine } from "@zag-js/solid" import * as tree from "@zag-js/tree-view" import { ChevronRightIcon, FileIcon, FolderIcon } from "lucide-solid" import { Accessor, createMemo, createUniqueId, Index, JSX, Show, } from "solid-js" // 1. Create the tree collection interface Node { id: string name: string children?: Node[] } const collection = tree.collection<Node>({ // ... }) // 2. Create the recursive tree node interface TreeNodeProps { node: Node indexPath: number[] api: Accessor<tree.Api> } const TreeNode = (props: TreeNodeProps): JSX.Element => { const { node, indexPath, api } = props const nodeProps = { indexPath, node } const nodeState = createMemo(() => api().getNodeState(nodeProps)) return ( <Show when={nodeState().isBranch} fallback={ <div {...api().getItemProps(nodeProps)}> <FileIcon /> {node.name} </div> } > <div {...api().getBranchProps(nodeProps)}> <div {...api().getBranchControlProps(nodeProps)}> <FolderIcon /> <span {...api().getBranchTextProps(nodeProps)}>{node.name}</span> <span {...api().getBranchIndicatorProps(nodeProps)}> <ChevronRightIcon /> </span> </div> <div {...api().getBranchContentProps(nodeProps)}> <div {...api().getBranchIndentGuideProps(nodeProps)} /> <Index each={node.children}> {(childNode, index) => ( <TreeNode node={childNode()} indexPath={[...indexPath, index]} api={api} /> )} </Index> </div> </div> </Show> ) } // 3. Create the tree view export function TreeView() { const service = useMachine(tree.machine, { id: createUniqueId(), collection }) const api = createMemo(() => tree.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <h3 {...api().getLabelProps()}>My Documents</h3> <div {...api().getTreeProps()}> <Index each={collection.rootNode.children}> {(node, index) => ( <TreeNode node={node()} indexPath={[index]} api={api} /> )} </Index> </div> </div> ) }
<!-- TreeNode.vue --> <script setup lang="ts"> import { FileIcon, FolderIcon, ChevronRightIcon } from "lucide-vue-next" import type { Api } from "@zag-js/tree-view" interface Node { id: string name: string children?: Node[] } interface Props { node: Node indexPath: number[] api: Api } const props = defineProps<Props>() const nodeProps = computed(() => ({ indexPath: props.indexPath, node: props.node, })) const nodeState = computed(() => props.api.getNodeState(nodeProps.value)) </script> <template> <template v-if="nodeState.isBranch"> <div v-bind="api.getBranchProps(nodeProps)"> <div v-bind="api.getBranchControlProps(nodeProps)"> <FolderIcon /> <span v-bind="api.getBranchTextProps(nodeProps)">{{ node.name }}</span> <span v-bind="api.getBranchIndicatorProps(nodeProps)"> <ChevronRightIcon /> </span> </div> <div v-bind="api.getBranchContentProps(nodeProps)"> <div v-bind="api.getBranchIndentGuideProps(nodeProps)" /> <TreeNode v-for="(childNode, index) in node.children" :key="childNode.id" :node="childNode" :index-path="[...indexPath, index]" :api="api" /> </div> </div> </template> <template v-else> <div v-bind="api.getItemProps(nodeProps)"><FileIcon /> {{ node.name }}</div> </template> </template>
<!-- TreeView.vue --> <script setup lang="ts"> import * as tree from "@zag-js/tree-view" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, useId } from "vue" // 1. Create the tree collection interface Node { id: string name: string children?: Node[] } const collection = tree.collection<Node>({ // ... }) const service = useMachine(tree.machine, { id: useId(), collection }) const api = computed(() => tree.connect(service, normalizeProps)) </script> <template> <main class="tree-view"> <div v-bind="api.getRootProps()"> <h3 v-bind="api.getLabelProps()">My Documents</h3> <div v-bind="api.getTreeProps()"> <TreeNode v-for="(node, index) in api.collection.rootNode.children" :key="node.id" :node="node" :index-path="[index]" :api="api" /> </div> </div> </main> </template>
<script lang="ts"> import { normalizeProps, useMachine } from "@zag-js/svelte" import * as tree from "@zag-js/tree-view" import { FileIcon, FolderIcon, ChevronRightIcon } from "lucide-svelte" // 1. Create the tree collection interface Node { id: string name: string children?: Node[] } const collection = tree.collection<Node>({ // ... }) // 2. Create the recursive tree node interface TreeNodeProps { node: Node indexPath: number[] api: tree.Api } const id = $props.id() const service = useMachine(tree.machine, { id, collection, }) const api = $derived(tree.connect(service, normalizeProps)) </script> {#snippet treeNode(nodeProps: TreeNodeProps)} {@const { node, indexPath, api } = nodeProps} {@const nodeState = api.getNodeState({ indexPath, node })} {#if nodeState.isBranch} <div {...api.getBranchProps({ indexPath, node })}> <div {...api.getBranchControlProps({ indexPath, node })}> <FolderIcon /> <span {...api.getBranchTextProps({ indexPath, node })}>{node.name}</span> <span {...api.getBranchIndicatorProps({ indexPath, node })}> <ChevronRightIcon /> </span> </div> <div {...api.getBranchContentProps({ indexPath, node })}> <div {...api.getBranchIndentGuideProps({ indexPath, node })}></div> {#each node.children || [] as childNode, index} {@render treeNode({ node: childNode, indexPath: [...indexPath, index], api })} {/each} </div> </div> {:else} <div {...api.getItemProps({ indexPath, node })}> <FileIcon /> {node.name} </div> {/if} {/snippet} <!-- 3. Create the tree view --> <div {...api.getRootProps()}> <h3 {...api.getLabelProps()}>My Documents</h3> <div {...api.getTreeProps()}> {#each collection.rootNode.children || [] as node, index} {@render treeNode({ node, indexPath: [index], api })} {/each} </div> </div>
Expanding and Collapsing Nodes
By default, the tree view will expand or collapse when clicking the branch
control. To control the expanded state of the tree view, use the api.expand
and api.collapse methods.
api.expand(["node_modules/pandacss"]) // expand a single node api.expand() // expand all nodes api.collapse(["node_modules/pandacss"]) // collapse a single node api.collapse() // collapse all nodes
Multiple selection
The tree view supports multiple selection. To enable this, set the
selectionMode to multiple.
const service = useMachine(tree.machine, { selectionMode: "multiple", })
Setting the default expanded nodes
To set the default expanded nodes, use the expandedValue context property.
const service = useMachine(tree.machine, { defaultExpandedValue: ["node_modules/pandacss"], })
Setting the default selected nodes
To set the default selected nodes, use the selectedValue context property.
const service = useMachine(tree.machine, { defaultSelectedValue: ["node_modules/pandacss"], })
Indentation Guide
When rendering a branch node in the tree view, you can render the indentGuide
element by using the api.getBranchIndentGuideProps() function.
<div {...api.getBranchProps(nodeProps)}> <div {...api.getBranchControlProps(nodeProps)}> <FolderIcon /> {node.name} <span {...api.getBranchIndicatorProps(nodeProps)}> <ChevronRightIcon /> </span> </div> <div {...api.getBranchContentProps(nodeProps)}> <div {...api.getBranchIndentGuideProps(nodeProps)} /> {node.children.map((childNode, index) => ( <TreeNode key={childNode.id} node={childNode} indexPath={[...indexPath, index]} api={api} /> ))} </div> </div>
Listening for selection
When a node is selected, the onSelectionChange callback is invoked with the
selected nodes.
const service = useMachine(tree.machine, { onSelectionChange(details) { console.log("selected nodes:", details) }, })
Listening for expanding and collapsing
When a node is expanded or collapsed, the onExpandedChange callback is invoked
with the expanded nodes.
const service = useMachine(tree.machine, { onExpandedChange(details) { console.log("expanded nodes:", details) }, })
Lazy Loading
Added in v1.15.0
Lazy loading is a feature that allows the tree view to load children of a node on demand. This helps to improve the initial load time and memory usage.
To use this, you need to provide the following:
loadChildren— A function that is used to load the children of a node.onLoadChildrenComplete— A callback that is called when the children of a node are loaded. Used to update the tree collection.childrenCount— A number that indicates the number of children of a branch node.
function TreeAsync() { const [collection, setCollection] = useState( tree.collection({ nodeToValue: (node) => node.id, nodeToString: (node) => node.name, rootNode: { id: "ROOT", name: "", children: [ { id: "node_modules", name: "node_modules", childrenCount: 3 }, { id: "src", name: "src", childrenCount: 2 }, ], }, }), ) const service = useMachine(tree.machine, { id: useId(), collection, async loadChildren({ valuePath, signal }) { const url = `/api/file-system/${valuePath.join("/")}` const response = await fetch(url, { signal }) const data = await response.json() return data.children }, onLoadChildrenComplete({ collection }) { setCollection(collection) }, }) // ... }
Methods and Properties
Machine Context
The tree view machine exposes the following context properties:
collectionTreeCollection<T>The tree collection dataidsPartial<{ root: string; tree: string; label: string; node: (value: string) => string; }>The ids of the tree elements. Useful for composition.expandedValuestring[]The controlled expanded node idsdefaultExpandedValuestring[]The initial expanded node ids when rendered. Use when you don't need to control the expanded node value.selectedValuestring[]The controlled selected node valuedefaultSelectedValuestring[]The initial selected node value when rendered. Use when you don't need to control the selected node value.defaultCheckedValuestring[]The initial checked node value when rendered. Use when you don't need to control the checked node value.checkedValuestring[]The controlled checked node valuedefaultFocusedValuestringThe initial focused node value when rendered. Use when you don't need to control the focused node value.focusedValuestringThe value of the focused nodeselectionMode"single" | "multiple"Whether the tree supports multiple selection - "single": only one node can be selected - "multiple": multiple nodes can be selectedonExpandedChange(details: ExpandedChangeDetails<T>) => voidCalled when the tree is opened or closedonSelectionChange(details: SelectionChangeDetails<T>) => voidCalled when the selection changesonFocusChange(details: FocusChangeDetails<T>) => voidCalled when the focused node changesonCheckedChange(details: CheckedChangeDetails) => voidCalled when the checked value changesonLoadChildrenComplete(details: LoadChildrenCompleteDetails<T>) => voidCalled when a node finishes loading childrenonLoadChildrenError(details: LoadChildrenErrorDetails<T>) => voidCalled when loading children fails for one or more nodesexpandOnClickbooleanWhether clicking on a branch should open it or nottypeaheadbooleanWhether the tree supports typeahead searchloadChildren(details: LoadChildrenDetails<T>) => Promise<T[]>Function to load children for a node asynchronously. When provided, branches will wait for this promise to resolve before expanding.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.
Machine API
The tree view api exposes the following methods:
collectionTreeCollection<V>The tree collection dataexpandedValuestring[]The value of the expanded nodes.setExpandedValue(value: string[]) => voidSets the expanded valueselectedValuestring[]The value of the selected nodes.setSelectedValue(value: string[]) => voidSets the selected valuecheckedValuestring[]The value of the checked nodestoggleChecked(value: string, isBranch: boolean) => voidToggles the checked value of a nodesetChecked(value: string[]) => voidSets the checked value of a nodeclearCheckedVoidFunctionClears the checked value of a nodegetCheckedMap() => CheckedValueMapReturns the checked details of branch and leaf nodesgetVisibleNodes() => V[]Returns the visible nodes as a flat array of nodes and their index pathexpand(value?: string[]) => voidFunction to expand nodes. If no value is provided, all nodes will be expandedcollapse(value?: string[]) => voidFunction to collapse nodes If no value is provided, all nodes will be collapsedselect(value?: string[]) => voidFunction to select nodes If no value is provided, all nodes will be selecteddeselect(value?: string[]) => voidFunction to deselect nodes If no value is provided, all nodes will be deselectedfocus(value: string) => voidFunction to focus a node by valueselectParent(value: string) => voidFunction to select the parent node of the focused nodeexpandParent(value: string) => voidFunction to expand the parent node of the focused node
Data Attributes
Accessibility
Adheres to the Tree View WAI-ARIA design pattern.
Keyboard Interactions
- TabMoves focus to the tree view, placing the first tree view item in focus.
- EnterSpaceSelects the item or branch node
- ArrowDownMoves focus to the next node
- ArrowUpMoves focus to the previous node
- ArrowRightWhen focus is on a closed branch node, opens the branch.
When focus is on an open branch node, moves focus to the first item node. - ArrowLeftWhen focus is on an open branch node, closes the node.
When focus is on an item or branch node, moves focus to its parent branch node. - HomeMoves focus to first node without opening or closing a node.
- EndMoves focus to the last node that can be focused without expanding any nodes that are closed.
- a-zA-ZFocus moves to the next node with a name that starts with the typed character. The search logic ignores nodes that are descendants of closed branch.
- *Expands all sibling nodes that are at the same depth as the focused node.
- Shift + ArrowDownMoves focus to and toggles the selection state of the next node.
- Shift + ArrowUpMoves focus to and toggles the selection state of the previous node.
- Ctrl + ASelects all nodes in the tree. If all nodes are selected, unselects all nodes.
Edit this page on GitHub