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-part
attribute 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
RootProvider
component, you don't need to use theRoot
component.
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 | boolean Use 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 | boolean Whether clicking on a branch should open it or not |
focusedValue | string The 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 | boolean Whether to enable lazy mounting |
onExpandedChange | (details: ExpandedChangeDetails) => void Called when the tree is opened or closed | |
onFocusChange | (details: FocusChangeDetails) => void Called when the focused node changes | |
onSelectionChange | (details: SelectionChangeDetails) => void Called 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 | boolean Whether the tree supports typeahead search |
unmountOnExit | false | boolean Whether to unmount on exit. |
BranchContent
Prop | Default | Type |
---|---|---|
asChild | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use 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 | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
lazyMount | false | boolean Whether to enable lazy mounting |
unmountOnExit | false | boolean Whether to unmount on exit. |
Tree
Prop | Default | Type |
---|---|---|
asChild | boolean Use 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. |