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