Tree View
A component that is used to show a tree hierarchy.
Anatomy
To set up the tree view component correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Examples
Learn how to use the TreeView component in your project. Let's take a look at the most basic example:
import { TreeView, createTreeCollection } from '@ark-ui/react/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-react'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const Basic = () => {
return (
<TreeView.Root collection={collection}>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
{collection.rootNode.children?.map((node, index) => <TreeNode key={node.id} node={node} indexPath={[index]} />)}
</TreeView.Tree>
</TreeView.Root>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
{node.children.map((child, index) => (
<TreeNode key={child.id} node={child} indexPath={[...indexPath, index]} />
))}
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}
import { TreeView, createTreeCollection } from '@ark-ui/solid/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-solid'
import { For, Show } from 'solid-js'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const Basic = () => {
return (
<TreeView.Root collection={collection}>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<For each={collection.rootNode.children}>{(node, index) => <TreeNode node={node} indexPath={[index()]} />}</For>
</TreeView.Tree>
</TreeView.Root>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider node={node} indexPath={indexPath}>
<Show
when={node.children}
fallback={
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
}
>
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
<For each={node.children}>
{(child, index) => <TreeNode node={child} indexPath={[...indexPath, index()]} />}
</For>
</TreeView.BranchContent>
</TreeView.Branch>
</Show>
</TreeView.NodeProvider>
)
}
<script setup lang="ts">
import { TreeView, createTreeCollection } from '@ark-ui/vue/tree-view'
import TreeNode from './tree-node.vue'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
</script>
<template>
<TreeView.Root :collection="collection">
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<TreeNode
v-for="(node, index) in collection.rootNode.children"
:key="node.id"
:node="node"
:indexPath="[index]"
/>
</TreeView.Tree>
</TreeView.Root>
</template>
Controlled Expanded
Pass the expandedValue and onExpandedChange props to the TreeView.Root component to control the expanded state of
the tree view.
import { TreeView, createTreeCollection } from '@ark-ui/react/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-react'
import { useState } from 'react'
export const ControlledExpanded = () => {
const [expandedValue, setExpandedValue] = useState<string[]>(['node_modules'])
return (
<TreeView.Root
collection={collection}
expandedValue={expandedValue}
onExpandedChange={({ expandedValue }) => setExpandedValue(expandedValue)}
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
{collection.rootNode.children?.map((node, index) => <TreeNode key={node.id} node={node} indexPath={[index]} />)}
</TreeView.Tree>
</TreeView.Root>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
{node.children.map((child, index) => (
<TreeNode key={child.id} node={child} indexPath={[...indexPath, index]} />
))}
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
import { TreeView, createTreeCollection } from '@ark-ui/solid/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-solid'
import { For, Show, createSignal } from 'solid-js'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const ControlledExpanded = () => {
const [expandedValue, setExpandedValue] = createSignal<string[]>(['node_modules'])
return (
<TreeView.Root
collection={collection}
expandedValue={expandedValue()}
onExpandedChange={({ expandedValue }) => setExpandedValue(expandedValue)}
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<For each={collection.rootNode.children}>{(node, index) => <TreeNode node={node} indexPath={[index()]} />}</For>
</TreeView.Tree>
</TreeView.Root>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider node={node} indexPath={indexPath}>
<Show
when={node.children}
fallback={
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
}
>
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
<For each={node.children}>
{(child, index) => <TreeNode node={child} indexPath={[...indexPath, index()]} />}
</For>
</TreeView.BranchContent>
</TreeView.Branch>
</Show>
</TreeView.NodeProvider>
)
}
<script setup lang="ts">
import { TreeView, createTreeCollection } from '@ark-ui/vue/tree-view'
import { ref } from 'vue'
import TreeNode from './tree-node.vue'
interface Node {
id: string
name: string
children?: Node[]
}
const expandedValue = ref<string[]>(['node_modules'])
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
</script>
<template>
<TreeView.Root :collection="collection" v-model:expanded-value="expandedValue">
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<TreeNode
v-for="(node, index) in collection.rootNode.children"
:key="node.id"
:node="node"
:indexPath="[index]"
/>
</TreeView.Tree>
</TreeView.Root>
</template>
Controlled Selection
Pass the selectedValue and onSelectionChange props to the TreeView.Root component to control the selected state of
the tree view.
import { TreeView, createTreeCollection } from '@ark-ui/react/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-react'
import { useState } from 'react'
export const ControlledSelected = () => {
const [selectedValue, setSelectedValue] = useState<string[]>(['package.json'])
return (
<TreeView.Root
collection={collection}
selectedValue={selectedValue}
onSelectionChange={({ selectedValue }) => setSelectedValue(selectedValue)}
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
{collection.rootNode.children?.map((node, index) => <TreeNode key={node.id} node={node} indexPath={[index]} />)}
</TreeView.Tree>
</TreeView.Root>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
{node.children.map((child, index) => (
<TreeNode key={child.id} node={child} indexPath={[...indexPath, index]} />
))}
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
import { TreeView, createTreeCollection } from '@ark-ui/solid/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-solid'
import { For, Show, createSignal } from 'solid-js'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const ControlledSelected = () => {
const [selectedValue, setSelectedValue] = createSignal<string[]>(['package.json'])
return (
<TreeView.Root
collection={collection}
selectedValue={selectedValue()}
onSelectionChange={({ selectedValue }) => setSelectedValue(selectedValue)}
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<For each={collection.rootNode.children}>{(node, index) => <TreeNode node={node} indexPath={[index()]} />}</For>
</TreeView.Tree>
</TreeView.Root>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider node={node} indexPath={indexPath}>
<Show
when={node.children}
fallback={
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
}
>
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
<For each={node.children}>
{(child, index) => <TreeNode node={child} indexPath={[...indexPath, index()]} />}
</For>
</TreeView.BranchContent>
</TreeView.Branch>
</Show>
</TreeView.NodeProvider>
)
}
<script setup lang="ts">
import { TreeView, createTreeCollection } from '@ark-ui/vue/tree-view'
import { ref } from 'vue'
import TreeNode from './tree-node.vue'
interface Node {
id: string
name: string
children?: Node[]
}
const selectedValue = ref<string[]>(['package.json'])
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
</script>
<template>
<TreeView.Root :collection="collection" v-model:selected-value="selectedValue">
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<TreeNode
v-for="(node, index) in collection.rootNode.children"
:key="node.id"
:node="node"
:indexPath="[index]"
/>
</TreeView.Tree>
</TreeView.Root>
</template>
Root Provider
Use the useTreeView hook to create the tree view store and pass it to the TreeView.RootProvider component. This
allows you to have maximum control over the tree view programmatically.
import { TreeView, createTreeCollection, useTreeView } from '@ark-ui/react/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-react'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const RootProvider = () => {
const treeView = useTreeView({ collection })
return (
<TreeView.RootProvider value={treeView}>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
{collection.rootNode.children?.map((node, index) => <TreeNode key={node.id} node={node} indexPath={[index]} />)}
</TreeView.Tree>
</TreeView.RootProvider>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
{node.children.map((child, index) => (
<TreeNode key={child.id} node={child} indexPath={[...indexPath, index]} />
))}
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}
import { TreeView, createTreeCollection, useTreeView } from '@ark-ui/solid/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-solid'
import { For, Show } from 'solid-js'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const RootProvider = () => {
const treeView = useTreeView({ collection })
return (
<TreeView.RootProvider value={treeView}>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<For each={collection.rootNode.children}>{(node, index) => <TreeNode node={node} indexPath={[index()]} />}</For>
</TreeView.Tree>
</TreeView.RootProvider>
)
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider node={node} indexPath={indexPath}>
<Show
when={node.children}
fallback={
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
}
>
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<FolderIcon /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
<For each={node.children}>
{(child, index) => <TreeNode node={child} indexPath={[...indexPath, index()]} />}
</For>
</TreeView.BranchContent>
</TreeView.Branch>
</Show>
</TreeView.NodeProvider>
)
}
<script setup lang="ts">
import { TreeView, createTreeCollection, useTreeView } from '@ark-ui/vue/tree-view'
import TreeNode from './tree-node.vue'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<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' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
const treeView = useTreeView({
collection,
})
</script>
<template>
<TreeView.RootProvider :value="treeView">
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<TreeNode
v-for="(node, index) in collection.rootNode.children"
:key="node.id"
:node="node"
:indexPath="[index]"
/>
</TreeView.Tree>
</TreeView.RootProvider>
</template>
If you're using the
RootProvidercomponent, you don't need to use theRootcomponent.
Lazy Loading
Lazy loading is a feature that allows the tree view to load children of a node on demand (or async). 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.
import { TreeView, createTreeCollection } from '@ark-ui/react/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon, Loader2Icon } from 'lucide-react'
import { useState } from 'react'
import { useTreeViewNodeContext } from '../use-tree-view-node-context'
// mock api result
const response: Record<string, Node[]> = {
node_modules: [
{ id: 'zag-js', name: 'zag-js' },
{ id: 'pandacss', name: 'panda' },
{ id: '@types', name: '@types', childrenCount: 2 },
],
'node_modules/@types': [
{ id: 'react', name: 'react' },
{ id: 'react-dom', name: 'react-dom' },
],
src: [
{ id: 'app.tsx', name: 'app.tsx' },
{ id: 'index.ts', name: 'index.ts' },
],
}
// function to load children of a node
function loadChildren(details: TreeView.LoadChildrenDetails<Node>): Promise<Node[]> {
const value = details.valuePath.join('/')
return new Promise((resolve) => {
setTimeout(() => {
resolve(response[value] ?? [])
}, 1200)
})
}
export const AsyncLoading = () => {
const [collection, setCollection] = useState(initialCollection)
return (
<TreeView.Root
collection={collection}
loadChildren={loadChildren}
onLoadChildrenComplete={(e) => setCollection(e.collection)}
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
{collection.rootNode.children?.map((node, index) => <TreeNode key={node.id} node={node} indexPath={[index]} />)}
</TreeView.Tree>
</TreeView.Root>
)
}
function TreeNodeIndicator() {
const nodeState = useTreeViewNodeContext()
return nodeState.loading ? <Loader2Icon style={{ animation: 'spin 1s infinite' }} /> : <FolderIcon />
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children || node.childrenCount ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<TreeNodeIndicator /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
{node.children?.map((child, index) => (
<TreeNode key={child.id} node={child} indexPath={[...indexPath, index]} />
))}
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}
interface Node {
id: string
name: string
children?: Node[]
childrenCount?: number
}
const initialCollection = createTreeCollection<Node>({
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 },
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
import { TreeView, createTreeCollection } from '@ark-ui/solid/tree-view'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon, Loader2Icon } from 'lucide-solid'
import { For, createSignal } from 'solid-js'
import { useTreeViewNodeContext } from '../use-tree-view-node-context'
// mock api result
const response: Record<string, Node[]> = {
node_modules: [
{ id: 'zag-js', name: 'zag-js' },
{ id: 'pandacss', name: 'panda' },
{ id: '@types', name: '@types', childrenCount: 2 },
],
'node_modules/@types': [
{ id: 'react', name: 'react' },
{ id: 'react-dom', name: 'react-dom' },
],
src: [
{ id: 'app.tsx', name: 'app.tsx' },
{ id: 'index.ts', name: 'index.ts' },
],
}
// function to load children of a node
function loadChildren(details: TreeView.LoadChildrenDetails<Node>): Promise<Node[]> {
const value = details.valuePath.join('/')
return new Promise((resolve) => {
setTimeout(() => {
resolve(response[value] ?? [])
}, 1200)
})
}
export const AsyncLoading = () => {
const [collection, setCollection] = createSignal(initialCollection)
return (
<TreeView.Root
collection={collection()}
loadChildren={loadChildren}
onLoadChildrenComplete={(e) => setCollection(e.collection)}
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<For each={collection().rootNode.children}>
{(node, index) => <TreeNode node={node} indexPath={[index()]} />}
</For>
</TreeView.Tree>
</TreeView.Root>
)
}
function TreeNodeIndicator() {
const nodeState = useTreeViewNodeContext()
return nodeState().loading ? <Loader2Icon style={{ animation: 'spin 1s infinite' }} /> : <FolderIcon />
}
const TreeNode = (props: TreeView.NodeProviderProps<Node>) => {
const { node, indexPath } = props
return (
<TreeView.NodeProvider node={node} indexPath={indexPath}>
{node.children || node.childrenCount ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchText>
<TreeNodeIndicator /> {node.name}
</TreeView.BranchText>
<TreeView.BranchIndicator>
<ChevronRightIcon />
</TreeView.BranchIndicator>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
<For each={node.children}>
{(child, index) => <TreeNode node={child} indexPath={[...indexPath, index()]} />}
</For>
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemIndicator>
<CheckSquareIcon />
</TreeView.ItemIndicator>
<TreeView.ItemText>
<FileIcon />
{node.name}
</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}
interface Node {
id: string
name: string
children?: Node[]
childrenCount?: number
}
const initialCollection = createTreeCollection<Node>({
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 },
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
<script setup lang="ts">
import {
type TreeCollection,
TreeView,
type TreeView as TreeViewTypes,
createTreeCollection,
} from '@ark-ui/vue/tree-view'
import { type Ref, ref } from 'vue'
import AsyncTreeNode from './async-tree-node.vue'
interface Node {
id: string
name: string
children?: Node[]
childrenCount?: number
}
// mock api result
const response: Record<string, Node[]> = {
node_modules: [
{ id: 'zag-js', name: 'zag-js' },
{ id: 'pandacss', name: 'panda' },
{ id: '@types', name: '@types', childrenCount: 2 },
],
'node_modules/@types': [
{ id: 'react', name: 'react' },
{ id: 'react-dom', name: 'react-dom' },
],
src: [
{ id: 'app.tsx', name: 'app.tsx' },
{ id: 'index.ts', name: 'index.ts' },
],
}
// function to load children of a node
function loadChildren(details: TreeViewTypes.LoadChildrenDetails<Node>): Promise<Node[]> {
const value = details.valuePath.join('/')
return new Promise((resolve) => {
setTimeout(() => {
resolve(response[value] ?? [])
}, 1200)
})
}
const initialCollection = createTreeCollection<Node>({
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 },
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
const collection = ref(initialCollection) as Ref<TreeCollection<Node>>
</script>
<template>
<TreeView.Root
:collection="collection"
:loadChildren="loadChildren"
@loadChildrenComplete="(e) => (collection = e.collection)"
>
<TreeView.Label>Tree</TreeView.Label>
<TreeView.Tree>
<AsyncTreeNode
v-for="(node, index) in collection.rootNode.children"
:key="node.id"
:node="node"
:indexPath="[index]"
/>
</TreeView.Tree>
</TreeView.Root>
</template>
API Reference
Root
| Prop | Default | Type |
|---|---|---|
collection | TreeCollection<T>The collection of tree nodes | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
defaultExpandedValue | string[]The initial expanded node ids when rendered. Use when you don't need to control the expanded node ids. | |
defaultSelectedValue | string[]The initial selected node ids when rendered. Use when you don't need to control the selected node ids. | |
expandedValue | string[]The controlled expanded node ids | |
expandOnClick | true | booleanWhether clicking on a branch should open it or not |
focusedValue | stringThe id of the focused node | |
ids | Partial<{ root: string; tree: string; label: string; node(value: string): string }>The ids of the tree elements. Useful for composition. | |
lazyMount | false | booleanWhether to enable lazy mounting |
onExpandedChange | (details: ExpandedChangeDetails) => voidCalled when the tree is opened or closed | |
onFocusChange | (details: FocusChangeDetails) => voidCalled when the focused node changes | |
onSelectionChange | (details: SelectionChangeDetails) => voidCalled when the selection changes | |
selectedValue | string[]The controlled selected node ids | |
selectionMode | 'single' | 'multiple' | 'single'Whether the tree supports multiple selection - "single": only one node can be selected - "multiple": multiple nodes can be selected |
typeahead | true | booleanWhether the tree supports typeahead search |
unmountOnExit | false | booleanWhether to unmount on exit. |
BranchContent
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[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 |
BranchControl
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[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-value] | The value of the item |
[data-depth] | The depth of the item |
BranchIndentGuide
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | tree-view |
[data-part] | branch-indent-guide |
[data-depth] | The depth of the item |
BranchIndicator
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[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 |
Branch
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[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 |
BranchText
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | tree-view |
[data-part] | branch-text |
[data-disabled] | Present when disabled |
[data-state] | "open" | "closed" |
BranchTrigger
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | tree-view |
[data-part] | branch-trigger |
[data-disabled] | Present when disabled |
[data-state] | "open" | "closed" |
[data-value] | The value of the item |
ItemIndicator
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | tree-view |
[data-part] | item-indicator |
[data-disabled] | Present when disabled |
[data-selected] | Present when selected |
[data-focus] | Present when focused |
Item
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[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-depth] | The depth of the item |
ItemText
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
| Data Attribute | Value |
|---|---|
[data-scope] | tree-view |
[data-part] | item-text |
[data-disabled] | Present when disabled |
[data-selected] | Present when selected |
[data-focus] | Present when focused |
Label
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
NodeProvider
| Prop | Default | Type |
|---|---|---|
indexPath | number[]The index path of the tree node | |
node | NonNullable<T>The tree node |
RootProvider
| Prop | Default | Type |
|---|---|---|
value | UseTreeViewReturn<T> | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
lazyMount | false | booleanWhether to enable lazy mounting |
unmountOnExit | false | booleanWhether to unmount on exit. |
Tree
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
Accessibility
Complies with the Tree View WAI-ARIA design pattern.
Keyboard Support
| Key | Description |
|---|---|
Tab | Moves focus to the tree view, placing the first tree view item in focus. |
EnterSpace | Selects the item or branch node |
ArrowDown | Moves focus to the next node |
ArrowUp | Moves focus to the previous node |
ArrowRight | When 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. |
ArrowLeft | When 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. |
Home | Moves focus to first node without opening or closing a node. |
End | Moves focus to the last node that can be focused without expanding any nodes that are closed. |
a-zA-Z | 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. |
* | Expands all sibling nodes that are at the same depth as the focused node. |
Shift + ArrowDown | Moves focus to and toggles the selection state of the next node. |
Shift + ArrowUp | Moves focus to and toggles the selection state of the previous node. |
Ctrl + A | Selects all nodes in the tree. If all nodes are selected, unselects all nodes. |