Tree is used to display hierarchical data.
import { Tree } from 'primereact/tree';<Tree>
<Tree.Header />
<Tree.List>
<Tree.Node>
<Tree.Content>
<Tree.Toggle />
</Tree.Content>
</Tree.Node>
</Tree.List>
<Tree.Footer />
<Tree.Empty />
</Tree>The basic example demonstrates a simple Tree implementation where nodes are expanded and collapsed on click.
'use client';
import { NodeService } from '@/services/node.service';
import type { TreeNode } from '@primereact/types/shared/tree';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function BasicDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([]);
React.useEffect(() => {
NodeService.getTreeNodes().then((data) => setNodes(data));
}, []);
return (
<Tree value={nodes} className="w-full md:w-120">
<Tree.List />
</Tree>
);
}
Tree nodes can be customized with templates using the Tree.Node components.
'use client';
import { Icon } from '@primereact/core/icon';
import type { CheckboxChangeEvent } from '@primereact/types/shared/checkbox';
import type { TreeContentInstance, TreeNode as TreeNodeType } from '@primereact/types/shared/tree';
import { Checkbox } from 'primereact/checkbox';
import { Tree } from 'primereact/tree';
import * as React from 'react';
const nodes = [
{
key: '0',
label: 'app',
data: 'app folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0',
label: 'api',
data: 'api folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0',
label: 'auth',
data: 'auth folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0-0',
label: 'route.ts',
data: 'route.ts file',
icon: 'pi pi-fw pi-file'
}
]
}
]
},
{
key: '0-1',
label: 'layout.tsx',
data: 'layout.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '0-2',
label: 'page.tsx',
data: 'page.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '1',
label: 'components',
data: 'components folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '1-0',
label: 'Header.tsx',
data: 'Header.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '1-1',
label: 'Footer.tsx',
data: 'Footer.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '2',
label: 'public',
data: 'public folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '2-0',
label: 'favicon.ico',
data: 'favicon.ico file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '3',
label: '.env.local',
data: '.env.local file',
icon: 'pi pi-fw pi-file'
},
{
key: '4',
label: 'next.config.js',
data: 'next.config.js file',
icon: 'pi pi-fw pi-file'
},
{
key: '5',
label: 'package.json',
data: 'package.json file',
icon: 'pi pi-fw pi-file'
},
{
key: '6',
label: 'README.md',
data: 'README.md file',
icon: 'pi pi-fw pi-file'
}
];
function CustomTreeNode({ node, index }: { node: TreeNodeType; index: number }) {
return (
<Tree.Node key={node.key} node={node} index={index}>
<Tree.Content>
{(instance: TreeContentInstance) => {
const { tree, treenode } = instance;
const leaf = treenode?.leaf;
const expanded = treenode?.expanded;
const checked = treenode?.checked;
const partialChecked = treenode?.partialChecked;
return (
<>
<Tree.Toggle>{expanded ? <Icon className="pi pi-arrow-up" /> : <Icon className="pi pi-arrow-down" />}</Tree.Toggle>
<Checkbox
checked={checked}
indeterminate={partialChecked}
onCheckedChange={(event: CheckboxChangeEvent) => {
tree?.onCheckboxChange(
event.originalEvent as React.ChangeEvent<HTMLInputElement>,
treenode?.props.node as TreeNodeType
);
}}
tabIndex={-1}
/>
{leaf ? (
<Tree.Icon className="pi pi-file" />
) : expanded ? (
<Tree.Icon className="pi pi-folder-open" />
) : (
<Tree.Icon className="pi pi-folder" />
)}
<Tree.Label>{node.label}</Tree.Label>
</>
);
}}
</Tree.Content>
{node.children && node.children.length > 0 && (
<Tree.List>
{node.children.map((childNode: TreeNodeType, childIndex: number) => (
<CustomTreeNode key={childNode.key} node={childNode} index={childIndex} />
))}
</Tree.List>
)}
</Tree.Node>
);
}
export default function NodeDemo() {
return (
<Tree className="w-full md:w-120" value={nodes} selectionMode="checkbox">
<Tree.List>
{nodes.map((node, index) => (
<CustomTreeNode key={node.key} node={node} index={index} />
))}
</Tree.List>
</Tree>
);
}
Tree state can be controlled programmatically with the expandedKeys property that defines the keys that are expanded. This property is a Map instance whose key is the key of a node and value is a boolean.
'use client';
import type { TreeExpandedKeys, TreeNode, useTreeExpandedChangeEvent } from '@primereact/types/shared/tree';
import { Button } from 'primereact/button';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function ControlledDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<TreeExpandedKeys>({ '0': true });
const nodes = [
{
key: '0',
label: 'app',
data: 'app folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0',
label: 'api',
data: 'api folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0',
label: 'auth',
data: 'auth folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0-0',
label: 'route.ts',
data: 'route.ts file',
icon: 'pi pi-fw pi-file'
}
]
}
]
},
{
key: '0-1',
label: 'layout.tsx',
data: 'layout.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '0-2',
label: 'page.tsx',
data: 'page.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '1',
label: 'components',
data: 'components folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '1-0',
label: 'Header.tsx',
data: 'Header.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '1-1',
label: 'Footer.tsx',
data: 'Footer.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '2',
label: 'public',
data: 'public folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '2-0',
label: 'favicon.ico',
data: 'favicon.ico file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '3',
label: '.env.local',
data: '.env.local file',
icon: 'pi pi-fw pi-file'
},
{
key: '4',
label: 'next.config.js',
data: 'next.config.js file',
icon: 'pi pi-fw pi-file'
},
{
key: '5',
label: 'package.json',
data: 'package.json file',
icon: 'pi pi-fw pi-file'
},
{
key: '6',
label: 'README.md',
data: 'README.md file',
icon: 'pi pi-fw pi-file'
}
];
const expandAll = () => {
const _expandedKeys = {};
for (const node of nodes) {
expandNode(node, _expandedKeys);
}
setExpandedKeys(_expandedKeys);
};
const collapseAll = () => {
setExpandedKeys({});
};
const expandNode = (node: TreeNode, _expandedKeys: TreeExpandedKeys) => {
if (node.children && node.children.length) {
_expandedKeys[node.key] = true;
for (const child of node.children) {
expandNode(child, _expandedKeys);
}
}
};
return (
<>
<div className="flex flex-wrap gap-2 mb-6">
<Button type="button" onClick={expandAll}>
<i className="pi pi-plus" />
Expand All
</Button>
<Button type="button" onClick={collapseAll}>
<i className="pi pi-minus" />
Collapse All
</Button>
</div>
<Tree
value={nodes}
expandedKeys={expandedKeys}
onExpandedChange={(e: useTreeExpandedChangeEvent) => setExpandedKeys(e.value)}
className="w-full md:w-120"
>
<Tree.List />
</Tree>
</>
);
}
Single node selection is configured by setting selectionMode as single along with selectionKeys property to manage the selection value binding.
'use client';
import { NodeService } from '@/services/node.service';
import type { TreeNode } from '@primereact/types/shared/tree';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function SingleSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([]);
React.useEffect(() => {
NodeService.getTreeNodes().then((data) => setNodes(data));
}, []);
return (
<Tree value={nodes} selectionMode="single" className="w-full md:w-120">
<Tree.List />
</Tree>
);
}
More than one node is selectable by setting selectionMode to multiple. By default in multiple selection mode, metaKey press (e.g. ⌘) is not necessary to add to existing selections. When the optional metaKeySelection is present, behavior is changed in a way that selecting a new node requires meta key to be present. Note that in touch enabled devices, Tree always ignores metaKey.
In multiple selection mode, value binding should be a key-value pair where key is the node key and value is a boolean to indicate selection.
'use client';
import { NodeService } from '@/services/node.service';
import { SwitchChangeEvent } from '@primereact/types/shared/switch';
import type { TreeNode } from '@primereact/types/shared/tree';
import { Label } from 'primereact/label';
import { Switch } from 'primereact/switch';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function SingleSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([]);
const [checked, setChecked] = React.useState(false);
React.useEffect(() => {
NodeService.getTreeNodes().then((data) => setNodes(data));
}, []);
return (
<>
<div className="flex items-center mb-4 gap-2">
<Switch checked={checked} onCheckedChange={(event: SwitchChangeEvent) => setChecked(event.checked)} inputId="input-metakey">
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
</Switch>
<Label htmlFor="input-metakey">MetaKey</Label>
</div>
<Tree value={nodes} selectionMode="multiple" metaKeySelection={checked} className="w-full md:w-120">
<Tree.List />
</Tree>
</>
);
}
Selection of multiple nodes via checkboxes is enabled by configuring selectionMode as checkbox.
In checkbox selection mode, value binding should be a key-value pair where key is the node key and value is an object that has checked and partialChecked properties to represent the checked state of a node object to indicate selection.
'use client';
import { NodeService } from '@/services/node.service';
import type { TreeNode } from '@primereact/types/shared/tree';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function CheckboxSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([]);
React.useEffect(() => {
NodeService.getTreeNodes().then((data) => setNodes(data));
}, []);
return (
<Tree value={nodes} selectionMode="checkbox" className="w-full md:w-120">
<Tree.List />
</Tree>
);
}
Lazy loading is demonstrated in this example where nodes are loaded on demand when a node is expanded.
'use client';
import { Icon } from '@primereact/core/icon';
import type { TreeNode, TreeNodeInstance, useTreeExpandEvent } from '@primereact/types/shared/tree';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function LazyDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([]);
const createLazyNodes = () => {
return [
{
key: '0',
label: 'Node 0',
leaf: false,
loading: true
},
{
key: '1',
label: 'Node 1',
leaf: false,
loading: true
},
{
key: '2',
label: 'Node 2',
leaf: false,
loading: true
}
];
};
const onNodeExpand = (event: useTreeExpandEvent) => {
const expandedNode = event.node;
if (!expandedNode.children) {
expandedNode.loading = true;
setNodes((prevNodes) => [...prevNodes]);
setTimeout(() => {
expandedNode.children = [
{
key: `${expandedNode.key}-0`,
label: `Lazy ${expandedNode.label} - 0`
},
{
key: `${expandedNode.key}-1`,
label: `Lazy ${expandedNode.label} - 1`
},
{
key: `${expandedNode.key}-2`,
label: `Lazy ${expandedNode.label} - 2`
}
];
expandedNode.loading = false;
setNodes((prevNodes) => [...prevNodes]);
}, 1000);
}
};
React.useEffect(() => {
setNodes(createLazyNodes());
setTimeout(() => {
setNodes((prevNodes) =>
prevNodes.map((node) => ({
...node,
loading: false
}))
);
}, 2000);
}, []);
return (
<Tree onExpand={onNodeExpand} className="w-full md:w-120">
<Tree.List>
{nodes.map((node, index) => (
<Tree.Node key={node.key} node={node} index={index}>
{(instance: TreeNodeInstance) => {
const { leaf, expanded } = instance;
return (
<>
<Tree.Content>
<Tree.Toggle>
{node.loading ? (
<Icon spin className="pi pi-spinner" />
) : expanded ? (
<Icon className="pi pi-minus-circle" />
) : (
<Icon className="pi pi-plus-circle" />
)}
</Tree.Toggle>
{leaf ? (
<Icon className="pi pi-file" />
) : expanded ? (
<Icon className="pi pi-folder-open" />
) : (
<Icon className="pi pi-folder" />
)}
<Tree.Label>{node.label}</Tree.Label>
</Tree.Content>
{node.children && expanded && <Tree.List />}
</>
);
}}
</Tree.Node>
))}
</Tree.List>
</Tree>
);
}
Filtering enables searching through the nodes. Place a Tree.Filter component inside Tree.Header to add a search input. Any input component can be used with the as prop and the filtering logic can be controlled with the onChange event.
'use client';
import { NodeService } from '@/services/node.service';
import type { TreeNode } from '@primereact/types/shared/tree';
import { Fluid } from 'primereact/fluid';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function FilterDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([]);
const [filterValue, setFilterValue] = React.useState<string>('');
const filterTree = React.useCallback((nodes: TreeNode[], query: string): TreeNode[] => {
if (!query) return nodes;
const filtered: TreeNode[] = [];
for (const node of nodes) {
const nodeMatches = node.label?.toString().toLowerCase().includes(query.toLowerCase());
const filteredChildren = node.children ? filterTree(node.children, query) : [];
if (nodeMatches || filteredChildren.length > 0) {
filtered.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : node.children
});
}
}
return filtered;
}, []);
const filteredNodes = React.useMemo(() => filterTree(nodes, filterValue), [nodes, filterValue, filterTree]);
React.useEffect(() => {
NodeService.getTreeNodes().then((data) => setNodes(data));
}, []);
return (
<Tree value={filteredNodes} className="w-full md:w-120">
<Tree.Header>
<IconField as={Fluid}>
<Tree.Filter
as={InputText}
value={filterValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterValue(e.target.value)}
/>
<IconField.Icon>
<i className="pi pi-search" />
</IconField.Icon>
</IconField>
</Tree.Header>
<Tree.List />
<Tree.Empty className="mt-2">No options found.</Tree.Empty>
</Tree>
);
}
Drag&Drop based reordering is enabled by adding the draggableNodes and droppableNodes properties..
'use client';
import type { TreeNode, useTreeValueChangeEvent } from '@primereact/types/shared/tree';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function DragDropSingleDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([
{
key: '0',
label: 'app',
data: 'app folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0',
label: 'api',
data: 'api folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0',
label: 'auth',
data: 'auth folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0-0',
label: 'route.ts',
data: 'route.ts file',
icon: 'pi pi-fw pi-file'
}
]
}
]
},
{
key: '0-1',
label: 'layout.tsx',
data: 'layout.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '0-2',
label: 'page.tsx',
data: 'page.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '1',
label: 'components',
data: 'components folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '1-0',
label: 'Header.tsx',
data: 'Header.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '1-1',
label: 'Footer.tsx',
data: 'Footer.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '2',
label: 'public',
data: 'public folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '2-0',
label: 'favicon.ico',
data: 'favicon.ico file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '3',
label: '.env.local',
data: '.env.local file',
icon: 'pi pi-fw pi-file'
},
{
key: '4',
label: 'next.config.js',
data: 'next.config.js file',
icon: 'pi pi-fw pi-file'
},
{
key: '5',
label: 'package.json',
data: 'package.json file',
icon: 'pi pi-fw pi-file'
},
{
key: '6',
label: 'README.md',
data: 'README.md file',
icon: 'pi pi-fw pi-file'
}
]);
return (
<Tree
value={nodes}
onValueChange={(e: useTreeValueChangeEvent) => setNodes(e.value)}
draggableNodes
droppableNodes
className="w-full md:w-120"
>
<Tree.List />
</Tree>
);
}
Nodes can be transferred between multiple trees as well. The draggableScope and droppableScope properties defines keys to restrict the actions between trees. In this example, nodes can only be transferred from start to the end.
'use client';
import type { TreeNode, useTreeValueChangeEvent } from '@primereact/types/shared/tree';
import { Tree } from 'primereact/tree';
import * as React from 'react';
export default function DragDropMultipleDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>([
{
key: '0',
label: 'app',
data: 'app folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0',
label: 'api',
data: 'api folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0',
label: 'auth',
data: 'auth folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '0-0-0-0',
label: 'route.ts',
data: 'route.ts file',
icon: 'pi pi-fw pi-file'
}
]
}
]
},
{
key: '0-1',
label: 'layout.tsx',
data: 'layout.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '0-2',
label: 'page.tsx',
data: 'page.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '1',
label: 'components',
data: 'components folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '1-0',
label: 'Header.tsx',
data: 'Header.tsx file',
icon: 'pi pi-fw pi-file'
},
{
key: '1-1',
label: 'Footer.tsx',
data: 'Footer.tsx file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '2',
label: 'public',
data: 'public folder',
icon: 'pi pi-fw pi-folder',
children: [
{
key: '2-0',
label: 'favicon.ico',
data: 'favicon.ico file',
icon: 'pi pi-fw pi-file'
}
]
},
{
key: '3',
label: '.env.local',
data: '.env.local file',
icon: 'pi pi-fw pi-file'
},
{
key: '4',
label: 'next.config.js',
data: 'next.config.js file',
icon: 'pi pi-fw pi-file'
},
{
key: '5',
label: 'package.json',
data: 'package.json file',
icon: 'pi pi-fw pi-file'
},
{
key: '6',
label: 'README.md',
data: 'README.md file',
icon: 'pi pi-fw pi-file'
}
]);
const [nodes2, setNodes2] = React.useState<TreeNode[]>([
{
key: '1-0',
label: '/etc',
icon: 'pi pi-fw pi-folder'
}
]);
const [nodes3, setNodes3] = React.useState<TreeNode[]>([]);
return (
<div className="flex flex-col md:flex-row gap-4">
<Tree
value={nodes}
onValueChange={(e: useTreeValueChangeEvent) => setNodes(e.value)}
draggableNodes
droppableNodes
draggableScope="first"
droppableScope="none"
className="flex-1 border border-surface rounded-lg"
>
<Tree.List />
<Tree.Empty>No Items Left</Tree.Empty>
</Tree>
<Tree
value={nodes2}
onValueChange={(e: useTreeValueChangeEvent) => setNodes2(e.value)}
draggableNodes
droppableNodes
draggableScope="second"
droppableScope="first"
className="flex-1 border border-surface rounded-lg"
pt={{
root: ({ state }) => ({ className: state.dragHover ? 'border-dashed border-primary!' : undefined })
}}
>
<Tree.List />
<Tree.Empty>Drag Nodes Here</Tree.Empty>
</Tree>
<Tree
value={nodes3}
onValueChange={(e: useTreeValueChangeEvent) => setNodes3(e.value)}
draggableNodes
droppableNodes
droppableScope={['first', 'second']}
className="flex-1 border border-surface rounded-lg"
pt={{
root: ({ state }) => ({ className: state.dragHover ? 'border-dashed border-primary!' : undefined })
}}
>
<Tree.List />
<Tree.Empty>Drag Nodes Here</Tree.Empty>
</Tree>
</div>
);
}
Value to describe the component can either be provided with aria-labelledby or aria-label props. The root list element has a tree role whereas each list item has a treeitem role along with aria-label,
aria-selected and aria-expanded attributes. In checkbox selection, aria-checked is used instead of aria-selected. The container element of a treenode has the group role. Checkbox and toggle icons are
hidden from screen readers as their parent element with treeitem role and attributes are used instead for readers and keyboard support. The aria-setsize, aria-posinset and aria-level attributes are calculated
implicitly and added to each treeitem.
| Key | Function |
|---|---|
tab | Moves focus to the first selected node when focus enters the component, if there is none then first element receives the focus. If focus is already inside the component, moves focus to the next focusable element in the page tab sequence. |
shift + tab | Moves focus to the last selected node when focus enters the component, if there is none then first element receives the focus. If focus is already inside the component, moves focus to the previous focusable element in the page tab sequence. |
enter | Selects the focused treenode. |
space | Selects the focused treenode. |
down arrow | Moves focus to the next treenode. |
up arrow | Moves focus to the previous treenode. |
right arrow | If node is closed, opens the node otherwise moves focus to the first child node. |
left arrow | If node is open, closes the node otherwise moves focus to the parent node. |