# 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

```jsx
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.

```ts
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.

```svelte
<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.

> **Note:** The array parameter contains individual node values/IDs, not paths.
> All nodes specified in the array will be collapsed or expanded.

```tsx
api.expand(["node_modules/zag-js"]) // expand a single node by its value/ID
api.expand(["node_modules/zag-js", "node_modules/pandacss"]) // expand multiple nodes by their values/IDs
api.expand() // expand all nodes

api.collapse(["node_modules/@types"]) // collapse a single node by its value/ID
api.collapse(["node_modules/@types", "node_modules/pandacss"]) // collapse multiple nodes by their values/IDs
api.collapse() // collapse all nodes
```

### Multiple selection

The tree view supports multiple selection. To enable this, set the
`selectionMode` to `multiple`.

```tsx {2}
const service = useMachine(tree.machine, {
  selectionMode: "multiple",
})
```

### Setting the default expanded nodes

To set the default expanded nodes, use the `expandedValue` context property.

```tsx {2}
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.

```tsx {2}
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.

```tsx {9}
<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.

```jsx {2-5}
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.

```jsx {2-5}
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.

```tsx
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)
    },
  })

  // ...
}
```

### Renaming Nodes

> **Unreleased** â Coming in the next version

The tree view supports renaming node labels inline, perfect for file explorers,
content management systems, and other applications where users need to edit item
names.

To enable renaming:

1. Add the rename input to your tree node component
2. Handle the `onRenameComplete` callback to update your collection

```svelte
<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 with state

  interface Node {
    id: string
    name: string
    children?: Node[]
  }

  const initialCollection = tree.collection<Node>({
    nodeToValue: (node) => node.id,
    nodeToString: (node) => node.name,
    rootNode: {
      id: "ROOT",
      name: "",
      children: [
        { id: "README.md", name: "README.md" },
        { id: "package.json", name: "package.json" },
      ],
    },
  })

  let collection = $state(initialCollection)

  // 2. Setup rename handlers

  const id = $props.id()
  const service = useMachine(tree.machine, {
    id,
    collection: () => collection,
    onRenameComplete: (details) => {
      // Update the collection with the new label
      const node = collection.at(details.indexPath)
      const updatedCollection = collection.replace(details.indexPath, {
        ...node,
        name: details.label,
      })
      collection = updatedCollection
    },
  })

  const api = $derived(tree.connect(service, normalizeProps))

  // 3. Create the tree node with rename input

  interface TreeNodeProps {
    node: Node
    indexPath: number[]
    api: tree.Api
  }
</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 })}
          style:display={nodeState.renaming ? "none" : "inline"}
        >
          {node.name}
        </span>
        <input {...api.getNodeRenameInputProps({ indexPath, node })} />
        <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 />
      <span
        {...api.getItemTextProps({ indexPath, node })}
        style:display={nodeState.renaming ? "none" : "inline"}
      >
        {node.name}
      </span>
      <input {...api.getNodeRenameInputProps({ indexPath, node })} />
    </div>
  {/if}
{/snippet}

<!-- 4. Create the tree view -->

<div {...api.getRootProps()}>
  <h3 {...api.getLabelProps()}>My Documents</h3>
  <p>Press F2 to rename any file or folder</p>
  <div {...api.getTreeProps()}>
    {#each collection.rootNode.children || [] as node, index}
      {@render treeNode({ node, indexPath: [index], api })}
    {/each}
  </div>
</div>
```

#### Controlling which nodes can be renamed

Use the `canRename` callback to control which nodes are renameable based on node
type or custom logic.

```tsx {3-6}
const service = useMachine(tree.machine, {
  collection,
  canRename(node, indexPath) {
    // Only allow renaming leaf nodes (files), not branches (folders)
    return !node.children
  },
})
```

#### Validating rename input

Use the `onBeforeRename` callback to validate the new name before accepting the
change. Return `false` to reject the rename.

```tsx {3-14}
const service = useMachine(tree.machine, {
  collection,
  onBeforeRename(details) {
    // Prevent empty names (label is already trimmed)
    if (!details.label) return false

    // Prevent duplicate names
    const parent = getParentNode(details.indexPath)
    const hasDuplicate = parent.children.some(
      (child) => child.name === details.label && child.id !== details.value
    )
    return !hasDuplicate
  },
})
```

#### Tracking rename events

Use the `onRenameStart` callback to track when users start renaming, useful for
analytics or showing contextual hints.

```tsx {3-6}
const service = useMachine(tree.machine, {
  collection,
  onRenameStart(details) {
    console.log("Started renaming:", details.node.name)
    trackEvent("tree_rename_started")
  },
})
```

**Features:**

- Press `F2` to enter rename mode
- Press `Enter` to submit or `Escape` to cancel
- Blur automatically submits changes
- Empty or whitespace-only names are automatically rejected
- Labels are automatically trimmed before callbacks
- IME composition events are properly handled
- `data-renaming` attribute added for styling

## Methods and Properties

### Machine Context

The tree view machine exposes the following context properties:

**`collection`**
Type: `TreeCollection<T>`
Description: The tree collection data

**`ids`**
Type: `Partial<{ root: string; tree: string; label: string; node: (value: string) => string; }>`
Description: The ids of the tree elements. Useful for composition.

**`expandedValue`**
Type: `string[]`
Description: The controlled expanded node ids

**`defaultExpandedValue`**
Type: `string[]`
Description: The initial expanded node ids when rendered.
Use when you don't need to control the expanded node value.

**`selectedValue`**
Type: `string[]`
Description: The controlled selected node value

**`defaultSelectedValue`**
Type: `string[]`
Description: The initial selected node value when rendered.
Use when you don't need to control the selected node value.

**`defaultCheckedValue`**
Type: `string[]`
Description: The initial checked node value when rendered.
Use when you don't need to control the checked node value.

**`checkedValue`**
Type: `string[]`
Description: The controlled checked node value

**`defaultFocusedValue`**
Type: `string`
Description: The initial focused node value when rendered.
Use when you don't need to control the focused node value.

**`focusedValue`**
Type: `string`
Description: The value of the focused node

**`selectionMode`**
Type: `"single" | "multiple"`
Description: Whether the tree supports multiple selection
- "single": only one node can be selected
- "multiple": multiple nodes can be selected

**`onExpandedChange`**
Type: `(details: ExpandedChangeDetails<T>) => void`
Description: Called when the tree is opened or closed

**`onSelectionChange`**
Type: `(details: SelectionChangeDetails<T>) => void`
Description: Called when the selection changes

**`onFocusChange`**
Type: `(details: FocusChangeDetails<T>) => void`
Description: Called when the focused node changes

**`onCheckedChange`**
Type: `(details: CheckedChangeDetails) => void`
Description: Called when the checked value changes

**`canRename`**
Type: `(node: T, indexPath: IndexPath) => boolean`
Description: Function to determine if a node can be renamed

**`onRenameStart`**
Type: `(details: RenameStartDetails<T>) => void`
Description: Called when a node starts being renamed

**`onBeforeRename`**
Type: `(details: RenameCompleteDetails) => boolean`
Description: Called before a rename is completed. Return false to prevent the rename.

**`onRenameComplete`**
Type: `(details: RenameCompleteDetails) => void`
Description: Called when a node label rename is completed

**`onLoadChildrenComplete`**
Type: `(details: LoadChildrenCompleteDetails<T>) => void`
Description: Called when a node finishes loading children

**`onLoadChildrenError`**
Type: `(details: LoadChildrenErrorDetails<T>) => void`
Description: Called when loading children fails for one or more nodes

**`expandOnClick`**
Type: `boolean`
Description: Whether clicking on a branch should open it or not

**`typeahead`**
Type: `boolean`
Description: Whether the tree supports typeahead search

**`loadChildren`**
Type: `(details: LoadChildrenDetails<T>) => Promise<T[]>`
Description: Function to load children for a node asynchronously.
When provided, branches will wait for this promise to resolve before expanding.

**`dir`**
Type: `"ltr" | "rtl"`
Description: The document's text/writing direction.

**`id`**
Type: `string`
Description: The unique identifier of the machine.

**`getRootNode`**
Type: `() => ShadowRoot | Node | Document`
Description: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.

### Machine API

The tree view `api` exposes the following methods:

**`collection`**
Type: `TreeCollection<V>`
Description: The tree collection data

**`expandedValue`**
Type: `string[]`
Description: The value of the expanded nodes.

**`setExpandedValue`**
Type: `(value: string[]) => void`
Description: Sets the expanded value

**`selectedValue`**
Type: `string[]`
Description: The value of the selected nodes.

**`setSelectedValue`**
Type: `(value: string[]) => void`
Description: Sets the selected value

**`checkedValue`**
Type: `string[]`
Description: The value of the checked nodes

**`toggleChecked`**
Type: `(value: string, isBranch: boolean) => void`
Description: Toggles the checked value of a node

**`setChecked`**
Type: `(value: string[]) => void`
Description: Sets the checked value of a node

**`clearChecked`**
Type: `VoidFunction`
Description: Clears the checked value of a node

**`getCheckedMap`**
Type: `() => CheckedValueMap`
Description: Returns the checked details of branch and leaf nodes

**`getVisibleNodes`**
Type: `() => V[]`
Description: Returns the visible nodes as a flat array of nodes and their index path

**`expand`**
Type: `(value?: string[]) => void`
Description: Function to expand nodes.
If no value is provided, all nodes will be expanded

**`collapse`**
Type: `(value?: string[]) => void`
Description: Function to collapse nodes
If no value is provided, all nodes will be collapsed

**`select`**
Type: `(value?: string[]) => void`
Description: Function to select nodes
If no value is provided, all nodes will be selected

**`deselect`**
Type: `(value?: string[]) => void`
Description: Function to deselect nodes
If no value is provided, all nodes will be deselected

**`focus`**
Type: `(value: string) => void`
Description: Function to focus a node by value

**`selectParent`**
Type: `(value: string) => void`
Description: Function to select the parent node of the focused node

**`expandParent`**
Type: `(value: string) => void`
Description: Function to expand the parent node of the focused node

**`startRenaming`**
Type: `(value: string) => void`
Description: Function to start renaming a node by value

**`submitRenaming`**
Type: `(value: string, label: string) => void`
Description: Function to submit the rename and update the node label

**`cancelRenaming`**
Type: `() => void`
Description: Function to cancel renaming without changes

### Data Attributes

**`Item`**

**`data-scope`**: tree-view
**`data-part`**: item
**`data-path`**: The path of the item
**`data-value`**: The value of the item
**`data-focus`**: Present when focused
**`data-selected`**: Present when selected
**`data-disabled`**: Present when disabled
**`data-renaming`**: 
**`data-depth`**: The depth of the item

**`ItemText`**

**`data-scope`**: tree-view
**`data-part`**: item-text
**`data-disabled`**: Present when disabled
**`data-selected`**: Present when selected
**`data-focus`**: Present when focused

**`ItemIndicator`**

**`data-scope`**: tree-view
**`data-part`**: item-indicator
**`data-disabled`**: Present when disabled
**`data-selected`**: Present when selected
**`data-focus`**: Present when focused

**`Branch`**

**`data-scope`**: tree-view
**`data-part`**: branch
**`data-depth`**: The depth of the item
**`data-branch`**: 
**`data-value`**: The value of the item
**`data-path`**: The path of the item
**`data-selected`**: Present when selected
**`data-state`**: "open" | "closed"
**`data-disabled`**: Present when disabled
**`data-loading`**: Present when loading

**`BranchIndicator`**

**`data-scope`**: tree-view
**`data-part`**: branch-indicator
**`data-state`**: "open" | "closed"
**`data-disabled`**: Present when disabled
**`data-selected`**: Present when selected
**`data-focus`**: Present when focused
**`data-loading`**: Present when loading

**`BranchTrigger`**

**`data-scope`**: tree-view
**`data-part`**: branch-trigger
**`data-disabled`**: Present when disabled
**`data-state`**: "open" | "closed"
**`data-value`**: The value of the item
**`data-loading`**: Present when loading

**`BranchControl`**

**`data-scope`**: tree-view
**`data-part`**: branch-control
**`data-path`**: The path of the item
**`data-state`**: "open" | "closed"
**`data-disabled`**: Present when disabled
**`data-selected`**: Present when selected
**`data-focus`**: Present when focused
**`data-renaming`**: 
**`data-value`**: The value of the item
**`data-depth`**: The depth of the item
**`data-loading`**: Present when loading

**`BranchText`**

**`data-scope`**: tree-view
**`data-part`**: branch-text
**`data-disabled`**: Present when disabled
**`data-state`**: "open" | "closed"
**`data-loading`**: Present when loading

**`BranchContent`**

**`data-scope`**: tree-view
**`data-part`**: branch-content
**`data-state`**: "open" | "closed"
**`data-depth`**: The depth of the item
**`data-path`**: The path of the item
**`data-value`**: The value of the item

**`BranchIndentGuide`**

**`data-scope`**: tree-view
**`data-part`**: branch-indent-guide
**`data-depth`**: The depth of the item

**`NodeCheckbox`**

**`data-scope`**: tree-view
**`data-part`**: node-checkbox
**`data-state`**: "checked" | "unchecked" | "indeterminate"
**`data-disabled`**: Present when disabled

### CSS Variables

<CssVarTable name="tree-view" />

## Accessibility

Adheres to the
[Tree View WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview).

### Keyboard Interactions

**`Tab`**
Description: Moves focus to the tree view, placing the first tree view item in focus.

**`Enter + Space`**
Description: Selects the item or branch node

**`ArrowDown`**
Description: Moves focus to the next node

**`ArrowUp`**
Description: Moves focus to the previous node

**`ArrowRight`**
Description: When focus is on a closed branch node, opens the branch.<br> When focus is on an open branch node, moves focus to the first item node.

**`ArrowLeft`**
Description: When focus is on an open branch node, closes the node.<br> When focus is on an item or branch node, moves focus to its parent branch node.

**`Home`**
Description: Moves focus to first node without opening or closing a node.

**`End`**
Description: Moves focus to the last node that can be focused without expanding any nodes that are closed.

**`a-z + A-Z`**
Description: Focus 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.

**`*`**
Description: Expands all sibling nodes that are at the same depth as the focused node.

**`Shift + ArrowDown`**
Description: Moves focus to and toggles the selection state of the next node.

**`Shift + ArrowUp`**
Description: Moves focus to and toggles the selection state of the previous node.

**`Ctrl + A`**
Description: Selects all nodes in the tree. If all nodes are selected, unselects all nodes.