Tree
A tree provides a hierarchical lists of data with nested expandable levels.
import * as React from 'react';
import { Tree, TreeNode } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const data = React.useMemo(
() => [
{
id: 'Node-0',
label: 'Node 0',
},
{
id: 'Node-1',
label: 'Node 1',
subItems: [{ id: 'Subnode-1', label: 'Subnode 1' }],
},
{
id: 'Node-2',
label: 'Node 2',
subItems: [{ id: 'Subnode-2', label: 'Subnode 2' }],
},
],
[],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
hasSubNodes: node.subItems?.length > 0,
};
},
[expandedNodes],
);
return (
<Tree
className='demo-tree'
data={data}
getNode={getNode}
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode label={node.label} onExpanded={onNodeExpanded} {...rest} />
),
[onNodeExpanded],
)}
/>
);
};
The Tree component can be used to organize data in an application specific way, or it can be used to sort, filter, group, or search data as the user deems appropriate.
Usage
To initialize the tree component, the following props are required:
data: An array of the custom data that represents a tree node.getNode: A function that maps yourdataentry toNodeDatathat has all information about the node state. Here is where one can control the state of expanded, selected and disabled nodes. The function must be memoized.nodeRenderer: A function to render the tree node usingNodeData. We recommend this function to return theTreeNodecomponent. This function must be memoized.
Note: When virtualization is enabled, the return value of nodeRenderer() is cloned and a ref is passed to it. Thus, you would need a React.forwardRef in the component returned by nodeRenderer(), except if you are returning TreeNode since that already forwards its ref.
Subnode
The tree supports hierarchial data structures where each node can have subnodes, which can be expanded or collapsed. Subnodes allow handling nested data up to any desired depth.
Each object in the data array can include an array of sub-item objects. This array can be named according to the user’s preference (e.g., subItems in the example below) to represent its children, enabling nested structures.
const treeData = React.useMemo( () => [ { id: 'Node-1', label: 'Node 1', subItems: [ { id: 'Node-1-1', label: 'Node 1.1' }, { id: 'Node-1-2', label: 'Node 1.2', subItems: [ { id: 'Node-1-2-1', label: 'Node 1.2.1' }, { id: 'Node-1-2-2', label: 'Node 1.2.2' }, ], }, ], }, { id: 'Node-2', label: 'Node 2', subItems: [{ id: 'Node-2-1', label: 'Node 2.1' }], }, ], [],);The getNode function then needs to map the user data to a NodeData. The properties relevant to sub-nodes include:
subNodes: array of childdatanodes. Can be obtained from thedata’ssubItems.hasSubNodes: indicates whether the node has subnodes which determines whether the nodes should be expandable.
const getNode = React.useCallback( (node) => { return { /* … */ subNodes: node.subItems, hasSubNodes: node.subItems.length > 0, }; }, [expandedNodes],);Expansion
A state variable can be used to track each node and its expansion state. The onExpand function in each TreeNode can be used to update the node’s expansion state accordingly.
const onExpand = React.useCallback((nodeId, isExpanded) => { setExpandedNodes((prev) => ({ ...prev, [nodeId]: isExpanded, }));}, []);The isExpanded flag which indicates whether the node is expanded to display its subnode(s) should be passed into the getNode function for each node to be updated its expansion state correctly.
const getNode = React.useCallback( (node) => { return { /* … */ isExpanded: expandedNodes[node.id], }; }, [expandedNodes],);import * as React from 'react';
import { Tree, TreeNode } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const generateItem = React.useCallback(
(index, parentNode = '', depth = 0) => {
const keyValue = parentNode ? `${parentNode}-${index}` : `${index}`;
return {
id: `Node-${keyValue}`,
label: `Node ${keyValue}`,
sublabel: `Sublabel for Node ${keyValue}`,
subItems:
depth < 10
? Array(Math.round(index % 5))
.fill(null)
.map((_, index) => generateItem(index, keyValue, depth + 1))
: [],
};
},
[],
);
const data = React.useMemo(
() =>
Array(3)
.fill(null)
.map((_, index) => generateItem(index)),
[generateItem],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
hasSubNodes: node.subItems.length > 0,
};
},
[expandedNodes],
);
return (
<Tree
className='demo-tree'
data={data}
getNode={getNode}
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode label={node.label} onExpanded={onNodeExpanded} {...rest} />
),
[onNodeExpanded],
)}
/>
);
};
Expander customization
The expander prop in the TreeNode component allows for customization of the node expanders. We recommend using the TreeNodeExpander component with this prop to customize the appearance and behavior of the expanders. If hasSubNodes is false, the expanders will not be shown.
import * as React from 'react';
import { Tree, TreeNode, TreeNodeExpander } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const disabledNodes = { 'Node-0': true, 'Node-2': true };
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const generateItem = React.useCallback(
(index, parentNode = '', depth = 0) => {
const keyValue = parentNode ? `${parentNode}-${index}` : `${index}`;
return {
id: `Node-${keyValue}`,
label: `Node ${keyValue}`,
sublabel: `Sublabel for Node ${keyValue}`,
subItems:
depth < 10
? Array(Math.round(index % 5))
.fill(null)
.map((_, index) => generateItem(index, keyValue, depth + 1))
: [],
};
},
[],
);
const data = React.useMemo(
() =>
Array(3)
.fill(null)
.map((_, index) => generateItem(index)),
[generateItem],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
isDisabled: Object.keys(disabledNodes).some(
(id) => node.id === id || node.id.startsWith(`${id}-`),
),
hasSubNodes: node.subItems.length > 0,
};
},
[expandedNodes],
);
return (
<Tree
className='demo-tree'
data={data}
getNode={getNode}
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode
label={node.label}
onExpanded={onNodeExpanded}
expander={
<TreeNodeExpander
isExpanded={rest.isExpanded}
onClick={(e) => {
onNodeExpanded(node.id, !rest.isExpanded);
e.stopPropagation();
}}
/>
}
{...rest}
/>
),
[onNodeExpanded],
)}
/>
);
};
Selection
The tree allows end users to select one or multiple nodes within its structure. This feature is useful for actions on specific nodes, such as editing, deleting or viewing details.
Similar to node expansion, a state variable can be used to track the currently selected node. This state can be updated via the onSelect callback which is triggered whenever a user selects a node. The isSelected flag must be set in the getNode function to correctly update each node’s selection state.
import React, { useCallback, useState } from 'react';
import { Tree, TreeNode } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const [selectedNodes, setSelectedNodes] = useState({});
const onSelectedNodeChange = useCallback((nodeId, isSelected) => {
setSelectedNodes((oldSelected) => ({
...oldSelected,
[nodeId]: isSelected,
}));
}, []);
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const generateItem = React.useCallback(
(index, parentNode = '', depth = 0) => {
const keyValue = parentNode ? `${parentNode}-${index}` : `${index}`;
return {
id: `Node-${keyValue}`,
label: `Node ${keyValue}`,
sublabel: `Sublabel for Node ${keyValue}`,
subItems:
depth < 10
? Array(Math.round(index % 5))
.fill(null)
.map((_, index) => generateItem(index, keyValue, depth + 1))
: [],
};
},
[],
);
const data = React.useMemo(
() =>
Array(3)
.fill(null)
.map((_, index) => generateItem(index)),
[generateItem],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
isSelected: selectedNodes[node.id],
hasSubNodes: node.subItems.length > 0,
};
},
[expandedNodes, selectedNodes],
);
return (
<Tree
className='demo-tree'
data={data}
getNode={getNode}
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode
label={node.label}
onExpanded={onNodeExpanded}
onSelected={onSelectedNodeChange}
{...rest}
/>
),
[onNodeExpanded, onSelectedNodeChange],
)}
/>
);
};
Size
There are two different sizes available. The default size should suffice for most cases. When a smaller version of the tree is needed, use size="small".
import * as React from 'react';
import { Tree, TreeNode } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const generateItem = React.useCallback(
(index, parentNode = '', depth = 0) => {
const keyValue = parentNode ? `${parentNode}-${index}` : `${index}`;
return {
id: `Node-${keyValue}`,
label: `Node ${keyValue}`,
subItems:
depth < 10
? Array(Math.round(index % 5))
.fill(null)
.map((_, index) => generateItem(index, keyValue, depth + 1))
: [],
};
},
[],
);
const data = React.useMemo(
() =>
Array(3)
.fill(null)
.map((_, index) => generateItem(index)),
[generateItem],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
hasSubNodes: node.subItems.length > 0,
};
},
[expandedNodes],
);
return (
<Tree
className='demo-tree'
data={data}
size='small'
getNode={getNode}
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode label={node.label} onExpanded={onNodeExpanded} {...rest} />
),
[onNodeExpanded],
)}
/>
);
};
Visibility checkbox
Each data level line may begin with an eye icon to toggle visibility. In this context, we suggest using the Checkbox component with the variant set to "eyeballs" and passing it into the checkbox prop of the TreeNode.
import * as React from 'react';
import { Checkbox, Tree, TreeNode } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const generateItem = React.useCallback(
(index, parentNode = '', depth = 0) => {
const keyValue = parentNode ? `${parentNode}-${index}` : `${index}`;
return {
id: `Node-${keyValue}`,
label: `Node ${keyValue}`,
sublabel: `Sublabel for Node ${keyValue}`,
subItems:
depth < 10
? Array(Math.round(index % 5))
.fill(null)
.map((_, index) => generateItem(index, keyValue, depth + 1))
: [],
};
},
[],
);
const data = React.useMemo(
() =>
Array(3)
.fill(null)
.map((_, index) => generateItem(index)),
[generateItem],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
hasSubNodes: node.subItems.length > 0,
};
},
[expandedNodes],
);
return (
<Tree
className='demo-tree'
data={data}
getNode={getNode}
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode
label={node.label}
onExpanded={onNodeExpanded}
checkbox={
<Checkbox
aria-label={node.label}
variant='eyeball'
disabled={rest.isDisabled}
/>
}
{...rest}
/>
),
[onNodeExpanded],
)}
/>
);
};
Virtualization
For trees with a large number of nodes, enabling virtualization can improve performance. To enable virtualization, the enableVirtualization property of the tree component can be set to true.
import * as React from 'react';
import { Tree, TreeNode } from '@itwin/itwinui-react';
export default () => {
const [expandedNodes, setExpandedNodes] = React.useState({});
const onNodeExpanded = React.useCallback((nodeId, isExpanded) => {
setExpandedNodes((oldExpanded) => ({
...oldExpanded,
[nodeId]: isExpanded,
}));
}, []);
const generateItem = React.useCallback(
(index, parentNode = '', depth = 0) => {
const keyValue = parentNode ? `${parentNode}-${index}` : `${index}`;
return {
id: `Node-${keyValue}`,
label: `Node ${keyValue}`,
sublabel: `Sublabel for Node ${keyValue}`,
subItems:
depth < 10
? Array(Math.round(index % 5))
.fill(null)
.map((_, index) => generateItem(index, keyValue, depth + 1))
: [],
};
},
[],
);
const data = React.useMemo(
() =>
Array(10000)
.fill(null)
.map((_, index) => generateItem(index)),
[generateItem],
);
const getNode = React.useCallback(
(node) => {
return {
subNodes: node.subItems,
nodeId: node.id,
node: node,
isExpanded: expandedNodes[node.id],
hasSubNodes: node.subItems.length > 0,
};
},
[expandedNodes],
);
return (
<Tree
className='demo-tree'
style={{ height: '400px' }}
data={data}
getNode={getNode}
enableVirtualization
nodeRenderer={React.useCallback(
({ node, ...rest }) => (
<TreeNode label={node.label} onExpanded={onNodeExpanded} {...rest} />
),
[onNodeExpanded],
)}
/>
);
};
Props
TreeNode
| Prop | Description | Default |
|---|---|---|
| nodeId | Unique id of the node.
It has to be compatible with HTML id attribute. string | |
| nodeProps | Props for main node inside the treeitem (excluding the sub-tree). If you need to customize the root node instead, pass top-level props directly to the TreeNode component.DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| label | The main text displayed on the node. ReactNode | |
| labelProps | Props for TreeNode label(affects both the main and sub label). DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| titleProps | Props for the TreeNode's main label. DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| sublabel | Small note displayed below main label. ReactNode | |
| sublabelProps | Props for TreeNode sublabel DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| icon | Icon shown before label and sublabel content. Element | |
| iconProps | Props for TreeNode Icon DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> | |
| hasSubNodes | Flag whether the node has child sub-nodes. It is used to show expander icon. boolean | false |
| subTreeProps | Props for subTree list(affects all subnodes of this node). DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| isDisabled | Flag whether the node is disabled. boolean | false |
| isExpanded | Flag whether the node is expanded. boolean | false |
| isSelected | Flag whether the node is selected. boolean | false |
| onExpanded | Callback fired when expanding or closing a TreeNode.
Gives nodeId and new isExpanded value of specified node. (nodeId: string, isExpanded: boolean) => void | |
| onSelected | Callback fired when selecting a TreeNode.
Gives nodeId and new isSelected value of specified node. (nodeId: string, isSelected: boolean) => void | |
| checkbox | Checkbox to be shown at the very beginning of the node.
If undefined, checkbox will not be shown.
Recommended to use Checkbox component.ReactNode | |
| checkboxProps | Props for TreeNode checkbox. DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| expander | Custom expander element. If hasSubNodes is false, it won't be shown.ReactNode | |
| expanderProps | Props for the default TreeNodeExpander that is shown when there are sub nodes and no expander is given. Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "label" | ... 10 more ... | "isActive"> & { ...; } & { ...; } & Omit<...> & { ...; } | |
| contentProps | Props for content of the TreeNode.
This affects all passed in children of the node, as well as the label, sublabel, icon, and expander.
Note that this does not affect the checkbox. DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | |
| children | Content shown after TreeNode.ReactNode | |
| id | @deprecated Use nodeId instead.never | |
| as | "symbol" | "object" | "div" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | "bdi" | "bdo" | "big" | "blockquote" | "body" | "br" | "button" | "canvas" | ... 159 more ... | FunctionComponent<...> |
TreeNodeExpander
| Prop | Description | Default |
|---|---|---|
| isExpanded | boolean | |
| expanderIconProps | SVGProps<SVGSVGElement> | |
| isActive | Button gets active style. boolean | false |
| label | Name of the button, shown in a tooltip and exposed to assistive technologies. ReactNode | |
| labelProps | Props passed to the Tooltip that contains the label.
Can be used for customizing the tooltip's placement, etc.Omit<Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "as" | "content" | "children" | "portal" | keyof TooltipOptions> & { ...; } & PortalProps & TooltipOptions & { ...; }, "ref">, "content" | ... 2 more ... | "ariaStrategy"> | |
| iconProps | Passes props to IconButton icon. DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> | |
| title | @deprecated Use label instead.string | |
| htmlDisabled | Built-in html disabled attributeboolean | |
| size | Modify size of the button. "small" | "large" | |
| styleType | Style of the button.
Use 'borderless' to hide outline. "default" | "cta" | "high-visibility" | "borderless" | 'default' |
| stretched | Whether the button should stretch to fill the width of the container. This is useful on narrow containers and mobile views. boolean | |
| as | "symbol" | "object" | "button" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | "bdi" | "bdo" | "big" | "blockquote" | "body" | "br" | "canvas" | ... 160 more ... | FunctionComponent<...> |
Tree
| Prop | Description | Default |
|---|---|---|
| size | Modify size of the tree. "default" | "small" | 'default' |
| nodeRenderer | Render function that should return the node element.
Recommended to use TreeNode component.Note: When virtualization is enabled, the return value of nodeRenderer() is cloned and a ref is passed to it. Thus, you would need a React.forwardRef in the component returned by nodeRenderer(), except if you are returning TreeNode since that already forwards its ref.(props: NodeRenderProps<T>) => Element | |
| data | Array of custom data used for TreeNodes inside Tree.T[] | |
| getNode | Function that maps your data entry to NodeData that has all info about the node state.
It will be used to render a tree node in nodeRenderer.
Must be memoized.(node: T) => NodeData<T> | |
| enableVirtualization | Virtualization is used to have a better performance with a lot of nodes. When enabled, Tree DOM structure will change - it will have a wrapper div to which className and style will be applied.
@betaboolean | false |
| id | string | |
| className | string | |
| style | CSSProperties |