Skip to main content
The @zag-js/collection package provides two data structure classes — ListCollection and TreeCollection — that components like Select, Combobox, and Tree View use internally to manage items. You construct a collection once and pass it to the machine as a prop. The collection exposes a stable API for traversal, querying, and manipulation that Zag uses to handle keyboard navigation, selection, and filtering.
ListCollection is used by the Select and Combobox components. TreeCollection is used by the Tree View component.

List collection

A ListCollection wraps a flat array of items and provides methods for searching, traversing, and querying values.

Creating a collection

import { ListCollection } from "@zag-js/collection"

const collection = new ListCollection({
  items: [
    { label: "Apple", value: "apple" },
    { label: "Banana", value: "banana" },
    { label: "Cherry", value: "cherry" },
  ],
})
Pass the collection to a machine that accepts it:
const service = useMachine(select.machine, {
  id: useId(),
  collection,
})

Finding items by value

const item = collection.find("banana")
// { label: "Banana", value: "banana" }

const items = collection.findMany(["apple", "cherry"])
// [{ label: "Apple", value: "apple" }, { label: "Cherry", value: "cherry" }]

Value traversal

Navigate to the next or previous value relative to a given one:
const nextValue = collection.getNextValue("apple")
// "banana"

const previousValue = collection.getPreviousValue("banana")
// "apple"
Jump directly to the first or last value:
collection.firstValue // "apple"
collection.lastValue  // "cherry"

Checking for value existence

collection.has("apple")  // true
collection.has("mango")  // false

Custom objects

If your items do not have label and value fields, provide mapping functions:
const collection = new ListCollection({
  items: [
    { id: 1, name: "Apple" },
    { id: 2, name: "Banana" },
    { id: 3, name: "Cherry" },
  ],
  itemToString: (item) => item.name,
  itemToValue: (item) => String(item.id),
})

Disabling items

Pass isItemDisabled to mark certain items as non-interactive:
const collection = new ListCollection({
  items: [
    { id: 1, name: "Apple" },
    { id: 2, name: "Banana" },
    { id: 3, name: "Cherry" },
  ],
  itemToString: (item) => item.name,
  itemToValue: (item) => String(item.id),
  isItemDisabled: (item) => item.id === 2,
})

Reordering items

Move an item from one index to another:
// Move Banana (index 1) before Apple (index 0)
collection.reorder(1, 0)

console.log(collection.items)
// [{ label: "Banana", ... }, { label: "Apple", ... }, { label: "Cherry", ... }]

Tree collection

A TreeCollection manages hierarchical data such as file systems, navigation trees, or organization charts. It provides traversal, querying, and mutation methods.

Creating a tree

import { TreeCollection } from "@zag-js/collection"

const tree = new TreeCollection({
  rootNode: {
    value: "root",
    label: "Root",
    children: [
      {
        value: "folder1",
        label: "Folder 1",
        children: [
          { value: "file1", label: "File 1.txt" },
          { value: "file2", label: "File 2.txt" },
        ],
      },
      {
        value: "folder2",
        label: "Folder 2",
        children: [
          {
            value: "subfolder1",
            label: "Subfolder 1",
            children: [{ value: "file3", label: "File 3.txt" }],
          },
        ],
      },
    ],
  },
})
tree.getFirstNode()?.value // "folder1"
tree.getLastNode()?.value  // "folder2"
tree.getNextNode("file1")?.value     // "file2"
tree.getPreviousNode("file2")?.value // "file1"
tree.getParentNode("file1")?.value
// "folder1"

tree.getParentNodes("file3").map((n) => n.value)
// ["folder2", "subfolder1"]
tree.getDescendantNodes("folder1").map((n) => n.value)
// ["file1", "file2"]

tree.getDescendantValues("folder2")
// ["subfolder1", "file3"]
const indexPath = tree.getIndexPath("file1") // [0, 0]

tree.getNextSibling(indexPath)?.value     // "file2"
tree.getPreviousSibling(indexPath)        // undefined
tree.getSiblingNodes(indexPath).map((n) => n.value)
// ["file1", "file2"]

Index path operations

Index paths uniquely identify a node’s position in the tree as an array of indices:
tree.getIndexPath("file3")   // [1, 0, 0]
tree.getValue([1, 0, 0])     // "file3"
tree.getValuePath([1, 0, 0]) // ["folder2", "subfolder1", "file3"]
tree.at([1, 0])?.value       // "subfolder1"

Branch and leaf detection

const folder1Node = tree.findNode("folder1")!
tree.isBranchNode(folder1Node) // true

tree.getBranchValues()
// ["folder1", "folder2", "subfolder1"]

Traversal with custom logic

tree.visit({
  onEnter(node, indexPath) {
    console.log(`${node.value} at depth ${indexPath.length}`)

    if (node.value === "folder2") {
      return "skip" // do not descend into this branch
    }
  },
})

Filtering

filter returns a new tree containing only nodes that satisfy the predicate. Branch nodes are kept if any of their descendants match.
const filesOnly = tree.filter((node) => !tree.isBranchNode(node))
filesOnly.getValues() // ["file1", "file2", "file3"]

Mutation

All mutation methods return a new TreeCollection instance. The original tree is not modified.
// Insert after a node
const indexPath = tree.getIndexPath("file1")!
const withNew = tree.insertAfter(indexPath, [{ value: "file1b", label: "File 1b.txt" }])

// Remove a node
const withoutFile2 = tree.remove([tree.getIndexPath("file2")!])

// Move file1 under folder2
const moved = tree.move(
  [tree.getIndexPath("file1")!],
  tree.getIndexPath("folder2")!,
)

// Replace a node
const replaced = tree.replace(
  tree.getIndexPath("file1")!,
  { value: "renamed", label: "Renamed.txt" },
)

Utility methods

// Flatten to array with depth info
tree.flatten().map((n) => ({ value: n.value, depth: n._indexPath.length }))

// All values in traversal order
tree.getValues()
// ["folder1", "file1", "file2", "folder2", "subfolder1", "file3"]

// Depth of a specific node
tree.getDepth("file3") // 3

Custom node types

Provide mapping functions if your nodes use different field names:
interface FileNode {
  id: string
  name: string
  items?: FileNode[]
  disabled?: boolean
}

const tree = new TreeCollection<FileNode>({
  rootNode: {
    id: "root",
    name: "Root",
    items: [
      { id: "1", name: "Document.pdf" },
      { id: "2", name: "Archive.zip", disabled: true },
    ],
  },
  nodeToValue: (node) => node.id,
  nodeToString: (node) => node.name,
  nodeToChildren: (node) => node.items,
  isNodeDisabled: (node) => node.disabled ?? false,
})

Creating a tree from file paths

The filePathToTree helper converts an array of slash-separated path strings into a TreeCollection:
import { filePathToTree } from "@zag-js/collection"

const paths = [
  "src/components/Button.tsx",
  "src/components/Input.tsx",
  "src/utils/helpers.ts",
  "docs/README.md",
]

const tree = filePathToTree(paths)
tree.getBranchValues() // ["src", "components", "utils", "docs"]

Build docs developers (and LLMs) love