A component for displaying hierarchical data in an expandable tree format, with support for selection, checkboxes, inline renaming, and keyboard navigation.
npm install @ark-ui/react'use client'
import { TreeView } from '@ark-ui/react/tree-view'
import { type Assign, type WithFixedClassName, createStyleContext } from '@pallas-ui/style-context'
import type { ComponentRef } from 'react'
import { type TreeViewVariantProps, treeView } from '@styled-system/recipes'
import type { ComponentProps, JsxStyleProps } from '@styled-system/types'
const { withProvider, withContext } = createStyleContext(treeView)
export type RootProps = WithFixedClassName<ComponentProps<typeof TreeView.Root>>
export const Root = withProvider<
ComponentRef<typeof TreeView.Root>,
Assign<RootProps, TreeViewVariantProps & JsxStyleProps>
>(TreeView.Root, 'root')
export const Label = withContext<
ComponentRef<typeof TreeView.Label>,
Assign<ComponentProps<typeof TreeView.Label>, JsxStyleProps>
>(TreeView.Label, 'label')
export const Tree = withContext<
ComponentRef<typeof TreeView.Tree>,
Assign<ComponentProps<typeof TreeView.Tree>, JsxStyleProps>
>(TreeView.Tree, 'tree')
export const Branch = withContext<
ComponentRef<typeof TreeView.Branch>,
Assign<ComponentProps<typeof TreeView.Branch>, JsxStyleProps>
>(TreeView.Branch, 'branch')
export const BranchControl = withContext<
ComponentRef<typeof TreeView.BranchControl>,
Assign<ComponentProps<typeof TreeView.BranchControl>, JsxStyleProps>
>(TreeView.BranchControl, 'branchControl')
export const BranchTrigger = withContext<
ComponentRef<typeof TreeView.BranchTrigger>,
Assign<ComponentProps<typeof TreeView.BranchTrigger>, JsxStyleProps>
>(TreeView.BranchTrigger, 'branchTrigger')
export const BranchIndicator = withContext<
ComponentRef<typeof TreeView.BranchIndicator>,
Assign<ComponentProps<typeof TreeView.BranchIndicator>, JsxStyleProps>
>(TreeView.BranchIndicator, 'branchIndicator')
export const BranchText = withContext<
ComponentRef<typeof TreeView.BranchText>,
Assign<ComponentProps<typeof TreeView.BranchText>, JsxStyleProps>
>(TreeView.BranchText, 'branchText')
export const BranchContent = withContext<
ComponentRef<typeof TreeView.BranchContent>,
Assign<ComponentProps<typeof TreeView.BranchContent>, JsxStyleProps>
>(TreeView.BranchContent, 'branchContent')
export const BranchIndentGuide = withContext<
ComponentRef<typeof TreeView.BranchIndentGuide>,
Assign<ComponentProps<typeof TreeView.BranchIndentGuide>, JsxStyleProps>
>(TreeView.BranchIndentGuide, 'branchIndentGuide')
export const Item = withContext<
ComponentRef<typeof TreeView.Item>,
Assign<ComponentProps<typeof TreeView.Item>, JsxStyleProps>
>(TreeView.Item, 'item')
export const ItemText = withContext<
ComponentRef<typeof TreeView.ItemText>,
Assign<ComponentProps<typeof TreeView.ItemText>, JsxStyleProps>
>(TreeView.ItemText, 'itemText')
export const ItemIndicator = withContext<
ComponentRef<typeof TreeView.ItemIndicator>,
Assign<ComponentProps<typeof TreeView.ItemIndicator>, JsxStyleProps>
>(TreeView.ItemIndicator, 'itemIndicator')
export const NodeProvider = TreeView.NodeProvider
export const NodeContext = TreeView.NodeContext
export const NodeCheckbox = withContext<
ComponentRef<typeof TreeView.NodeCheckbox>,
Assign<ComponentProps<typeof TreeView.NodeCheckbox>, JsxStyleProps>
>(TreeView.NodeCheckbox, 'nodeCheckbox')
export const NodeCheckboxIndicator = withContext<
ComponentRef<typeof TreeView.NodeCheckboxIndicator>,
Assign<ComponentProps<typeof TreeView.NodeCheckboxIndicator>, JsxStyleProps>
>(TreeView.NodeCheckboxIndicator, 'nodeCheckboxIndicator')
export const NodeRenameInput = withContext<
ComponentRef<typeof TreeView.NodeRenameInput>,
Assign<ComponentProps<typeof TreeView.NodeRenameInput>, JsxStyleProps>
>(TreeView.NodeRenameInput, 'nodeRenameInput')
const TreeViewComponent = {
Root,
Label,
Tree,
NodeProvider,
NodeContext,
Branch,
BranchControl,
BranchTrigger,
BranchIndicator,
BranchText,
BranchContent,
BranchIndentGuide,
Item,
ItemText,
ItemIndicator,
NodeCheckbox,
NodeCheckboxIndicator,
NodeRenameInput,
}
export { createTreeCollection } from '@ark-ui/react/tree-view'
export default TreeViewComponentimport TreeView, { createTreeCollection } from '@/components/ui/tree-view'Use createTreeCollection to transform your data into a tree structure the component understands. Then wrap the output in TreeView.Root and render each node using TreeView.NodeProvider.
interface FileNode {
id: string
name: string
children?: FileNode[]
}
const collection = createTreeCollection<FileNode>({
nodeToValue: (node) => node.id,
nodeToString: (node) => node.name,
nodeToChildren: (node) => node.children ?? [],
rootNode: {
id: 'ROOT',
name: '',
children: [
{
id: 'src',
name: 'src',
children: [
{ id: 'src/index', name: 'index.tsx' },
],
},
],
},
})
<TreeView.Root collection={collection}>
<TreeView.Label>Files</TreeView.Label>
<TreeView.Tree>
<TreeView.NodeProvider node={collection.rootNode.children[0]} indexPath={[0]}>
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchTrigger>
<TreeView.BranchIndicator />
</TreeView.BranchTrigger>
<TreeView.BranchText>src</TreeView.BranchText>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.NodeProvider node={collection.rootNode.children[0].children[0]} indexPath={[0, 0]}>
<TreeView.Item>
<TreeView.ItemText>index.tsx</TreeView.ItemText>
</TreeView.Item>
</TreeView.NodeProvider>
</TreeView.BranchContent>
</TreeView.Branch>
</TreeView.NodeProvider>
</TreeView.Tree>
</TreeView.Root>For real-world usage, a recursive renderNode function is the recommended pattern:
function renderNode(node: FileNode, indexPath: number[]) {
return (
<TreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children ? (
<TreeView.Branch>
<TreeView.BranchControl>
<TreeView.BranchTrigger>
<TreeView.BranchIndicator />
</TreeView.BranchTrigger>
<TreeView.BranchText>{node.name}</TreeView.BranchText>
</TreeView.BranchControl>
<TreeView.BranchContent>
{node.children.map((child, i) => renderNode(child, [...indexPath, i]))}
</TreeView.BranchContent>
</TreeView.Branch>
) : (
<TreeView.Item>
<TreeView.ItemText>{node.name}</TreeView.ItemText>
</TreeView.Item>
)}
</TreeView.NodeProvider>
)
}Three sizes are available: xs, sm, and md (default). The size controls text size, padding, and icon dimensions throughout the tree.
Three visual variants control the hover and selection appearance: subtle (default), outline, and solid.
Add TreeView.BranchIndentGuide as the first child of TreeView.BranchContent to render a vertical line that visually connects parent nodes to their children.
Add TreeView.NodeCheckbox and TreeView.NodeCheckboxIndicator to each node to enable multi-select with checkboxes. Branch nodes automatically track the indeterminate state when their children are partially checked — pass the indeterminate character as the indeterminate prop.
Pass an isNodeDisabled callback to createTreeCollection to mark specific nodes as non-interactive. Disabled nodes are still visible but cannot be focused, selected, or expanded.
Control both the expanded and selected state externally by passing expandedValue and selectedValue alongside their change handlers. This is useful when you need to sync the tree state with other parts of your UI.
Enable inline renaming by passing canRename to TreeView.Root. Add TreeView.NodeRenameInput inside each node — it will appear automatically when the node enters rename mode. Press F2 to activate rename mode on the focused node. Press Enter to confirm or Escape to cancel.
By using controlled expandedValue state, you can programmatically expand or collapse all branches at once by setting the value to an array of all branch IDs (expand) or an empty array (collapse).
| Key | Description |
|---|---|
Tab | Brings focus into the tree, targeting the first item. |
Enter Space | Selects the focused item or branch. |
ArrowDown | Focuses the next visible node. |
ArrowUp | Focuses the previous visible node. |
ArrowRight | Opens a collapsed branch. If the branch is already open, focuses its first child. |
ArrowLeft | Closes an open branch. If already closed, moves focus up to the parent branch. |
Home | Jumps focus to the very first node in the tree without affecting expand/collapse state. |
End | Jumps focus to the last reachable node, skipping past any collapsed branches. |
a-z A-Z | Typeahead — jumps to the next node whose label begins with the typed character. Closed branch descendants are excluded from the search. |
* | Expands all branches at the same depth as the currently focused node. |
Shift + ArrowDown | Extends the selection downward, toggling the next node's selected state. |
Shift + ArrowUp | Extends the selection upward, toggling the previous node's selected state. |
Ctrl + A | Toggles selection of all nodes — selects everything if any are unselected, otherwise clears all. |
| Prop | Type | Default | Description |
|---|---|---|---|
collection | TreeCollection<T> | required | Tree data built with createTreeCollection |
size | 'xs' | 'sm' | 'md' | 'md' | Controls text size, padding, and icon sizes |
variant | 'subtle' | 'outline' | 'solid' | 'subtle' | Visual style of hover and selection states |
defaultExpandedValue | string[] | [] | Node IDs expanded on initial render |
expandedValue | string[] | — | Controlled expanded state |
onExpandedChange | (details: { expandedValue: string[] }) => void | — | Fired when any node expands or collapses |
defaultSelectedValue | string[] | [] | Node IDs selected on initial render |
selectedValue | string[] | — | Controlled selection state |
onSelectionChange | (details: { selectedValue: string[] }) => void | — | Fired when selection changes |
selectionMode | 'single' | 'multiple' | 'single' | Single or multi-node selection |
defaultCheckedValue | string[] | [] | Node IDs checked on initial render (checkbox mode) |
checkedValue | string[] | — | Controlled checked state |
onCheckedChange | (details: { checkedValue: string[] }) => void | — | Fired when checked nodes change |
canRename | (node: T, indexPath: IndexPath) => boolean | — | Determines if a node supports inline renaming |
onRenameComplete | (details: RenameCompleteDetails) => void | — | Fired when a rename is committed |
expandOnClick | boolean | true | Whether clicking a branch label expands it |
typeahead | boolean | true | Enables keyboard typeahead navigation |
lazyMount | boolean | false | Defers rendering collapsed branch content until first expansion |
unmountOnExit | boolean | false | Removes collapsed branch content from the DOM |
| Option | Type | Required | Description |
|---|---|---|---|
rootNode | T | Yes | Root of your data tree. Its children become the top-level nodes; the root itself is not rendered |
nodeToValue | (node: T) => string | Yes | Extracts a unique string ID for each node |
nodeToString | (node: T) => string | Yes | Extracts the display label for each node |
nodeToChildren | (node: T) => T[] | No | Extracts the children array from a node. Omit for flat lists |
isNodeDisabled | (node: T) => boolean | No | Return true to mark a node as non-interactive |
All sub-components accept the same props as their underlying Ark UI counterpart plus PandaCSS style props. The table below documents the rendered element and purpose of each.
| Sub-component | Renders | Purpose |
|---|---|---|
TreeView.Label | <h3> | Accessible label for the tree |
TreeView.Tree | <div> | Root list container |
TreeView.Branch | <div> | A node that has children |
TreeView.BranchControl | <div> | Clickable row for a branch node |
TreeView.BranchTrigger | <div> | Clickable control that toggles expand/collapse |
TreeView.BranchIndicator | <div> | Animated chevron/icon slot |
TreeView.BranchText | <span> | Label text for a branch |
TreeView.BranchContent | <div> | Container for child nodes |
TreeView.BranchIndentGuide | <div> | Vertical indent line |
TreeView.Item | <div> | A leaf node (no children) |
TreeView.ItemText | <span> | Label text for a leaf node |
TreeView.ItemIndicator | <div> | Icon/indicator slot for leaf nodes |
TreeView.NodeCheckbox | <span> | Checkbox control for multi-select |
TreeView.NodeCheckboxIndicator | — | Visual checkmark/indeterminate indicator |
TreeView.NodeRenameInput | <input> | Inline rename input, shown in rename mode |
TreeView.NodeProvider | — | Context provider — wraps each node to supply tree state |
TreeView.NodeContext | — | Render-prop component — provides current node's state to children |
Expanded
src
Selected
none