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 yourdata
entry toNodeData
that 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 theTreeNode
component. 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.
The getNode
function then needs to map the user data
to a NodeData
. The properties relevant to sub-nodes include:
subNodes
: array of childdata
nodes. Can be obtained from thedata
’ssubItems
.hasSubNodes
: indicates whether the node has subnodes which determines whether the nodes should be expandable.
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.
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.
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'
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<Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & { ...; }, "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" | ... 158 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<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ...; }, "as" | ... 3 more ... | 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" | ... 159 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 |