Skip to Content

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 your data entry to NodeData 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 using NodeData. We recommend this function to return the TreeNode 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.

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 child data nodes. Can be obtained from the data’s subItems.
  • 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'
      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 attribute
boolean
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. @beta
boolean
false
id
string
className
string
style
CSSProperties