For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/en/llms-full.txt, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/en/components/tree.md.

Tree

Browse, select and check hierarchical content.

Browse and select

Design resources
Component guidelines
Brand colors
Archived resources (disabled)
Selected: None
basic.tsx
import { Space, Text, Tree } from '@ifhover/leaf-ui';

import { useState } from 'react';

import { treeData } from './data';

export function TreeBasic() {
  const [selected, setSelected] = useState<readonly string[]>([]);
  return (
    <Space direction="vertical" align="stretch">
      <Tree
        data={treeData}
        defaultExpandedKeys={['design']}
        showIcon
        selectedKeys={selected}
        onSelect={setSelected}
      />
      <output>
        <Text tone="muted">Selected: {selected.join(', ') || 'None'}</Text>
      </output>
    </Space>
  );
}

Linked checks

Design resources
Component guidelines
Brand colors
Archived resources (disabled)
Engineering
Website
Mobile app
Checked keys: components
checkable.tsx
import { Space, Text, Tree } from '@ifhover/leaf-ui';

import { useState } from 'react';

import { treeData } from './data';

export function TreeCheckable() {
  const [checked, setChecked] = useState<readonly string[]>(['components']);
  return (
    <Space direction="vertical" align="stretch">
      <Tree
        data={treeData}
        checkable
        selectable={false}
        defaultExpandAll
        showLine
        checkedKeys={checked}
        onCheck={setChecked}
      />
      <output>
        <Text tone="muted">Checked keys: {checked.join(', ') || 'None'}</Text>
      </output>
    </Space>
  );
}

Independent checks

Parents and children are checked independently.

Design resources
Component guidelines
Brand colors
Archived resources (disabled)
Engineering
Website
Mobile app
strict.tsx
import { Tree } from '@ifhover/leaf-ui';

import { treeData } from './data';

export function TreeStrict() {
  return <Tree data={treeData} checkable checkStrictly selectable={false} defaultExpandAll />;
}

Search nodes

search.tsx
import { Input, Space, Tree } from '@ifhover/leaf-ui';

import { Search } from 'lucide-react';

import { useState } from 'react';

import { treeData } from './data';

export function TreeSearch() {
  const [query, setQuery] = useState('');
  return (
    <Space direction="vertical" align="stretch">
      <Input
        aria-label="Search nodes"
        placeholder="Search nodes"
        prefix={<Search />}
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        allowClear
      />
      <Tree data={treeData} searchValue={query} showIcon />
    </Space>
  );
}

Extended features

Drag a handle to the upper, middle or lower part of a target row to place a node before, inside or after it. With the keyboard, Space picks up or drops, Up/Down chooses a target, Left/Right places before/after it, and Escape cancels. Update data with the new tree returned by onDrop.

Expand Projects to load children. Move nodes using their handles.

10000 nodes, only visible rows are mounted.

Node 1
Node 2
Node 3
Node 4
Node 5
Node 6
Node 7
Node 8
Node 9
Node 10
Node 11
extensions.tsx
import type { TreeNode } from '@ifhover/leaf-ui';

import { Space, Tree } from '@ifhover/leaf-ui';

import { useState } from 'react';

const initial: TreeNode[] = [
  { key: 'projects', title: 'Projects', isLeaf: false },
  { key: 'archive', title: 'Archive', children: [{ key: 'draft', title: 'Draft', isLeaf: true }] },
];

export function ExtensionDemo() {
  const [data, setData] = useState(initial);
  return (
    <Space direction="vertical" align="stretch">
      <p>Expand Projects to load children. Move nodes using their handles.</p>
      <Tree
        data={data}
        loadData={async (node, signal) => {
          await new Promise<void>((resolve, reject) => {
            const timer = setTimeout(resolve, 500);
            signal.addEventListener(
              'abort',
              () => {
                clearTimeout(timer);
                reject(new DOMException('Aborted', 'AbortError'));
              },
              { once: true },
            );
          });
          return [
            { key: `${node.key}-design`, title: 'Design', isLeaf: true },
            { key: `${node.key}-dev`, title: 'Development', isLeaf: true },
          ];
        }}
        draggable
        onDrop={(info) => setData(info.data)}
      />
      <p>10000 nodes, only visible rows are mounted.</p>
      <Tree
        virtual
        height={240}
        data={Array.from({ length: 10000 }, (_, i) => ({
          key: String(i),
          title: `Node ${i + 1}`,
        }))}
      />
    </Space>
  );
}

Refreshing asynchronous data

Update cacheKey to refresh lazy caches for the same node keys. Changing data or loadData also clears caches; keep their references stable across unrelated renders. Cancelled requests cannot overwrite the new data.

Documents
The root key stays stable. A new cache version discards old children and cancels pending requests.
capabilities.tsx
import { Button, Space, Text, Tree, type TreeNode } from '@ifhover/leaf-ui';

import { useCallback, useMemo, useState } from 'react';

export function TreeCacheRefresh() {
  const [version, setVersion] = useState(1);
  const data = useMemo<readonly TreeNode[]>(
    () => [{ key: 'root', title: 'Documents', isLeaf: false }],
    [true],
  );
  const loadData = useCallback(
    async (node: TreeNode, signal: AbortSignal): Promise<readonly TreeNode[]> => {
      await new Promise<void>((resolve) => {
        const timer = setTimeout(resolve, 400);
        signal.addEventListener(
          'abort',
          () => {
            clearTimeout(timer);
            resolve();
          },
          { once: true },
        );
      });
      if (signal.aborted) return [];
      return [
        {
          key: `${node.key}-${version}`,
          title: `${'Release'} ${version}`,
          isLeaf: true,
        },
      ];
    },
    [true, version],
  );
  return (
    <Space direction="vertical" align="stretch">
      <div>
        <Button variant="outline" onClick={() => setVersion((value) => value + 1)}>
          {'Refresh data'}
        </Button>
      </div>
      <Tree
        aria-label={'Versioned documents'}
        data={data}
        loadData={loadData}
        cacheKey={version}
        defaultExpandedKeys={['root']}
      />
      <Text tone="muted">
        {'The root key stays stable. A new cache version discards old children and cancels pending requests.'}
      </Text>
    </Space>
  );
}

Tree API

PropertyTypeDefaultDescription
datareadonly TreeNode[]RequiredRequired. Tree nodes
selectedKeysreadonly string[][]Controlled / initial selected keys
defaultSelectedKeysreadonly string[][]Controlled / initial selected keys
onSelect(keys: string[], info: TreeSelectInfo) => void—Selection changes
multiplebooleanfalseMultiple selection / allow selection
selectablebooleantrueMultiple selection / allow selection
checkablebooleanfalseShow checkboxes / disable linked checks
checkStrictlybooleanfalseShow checkboxes / disable linked checks
checkedKeysreadonly string[][]Controlled / initial checked keys
defaultCheckedKeysreadonly string[][]Controlled / initial checked keys
onCheck(keys: string[], info: TreeCheckInfo) => void—Check changes, including linked results
expandedKeysreadonly string[][]Controlled / initial expanded keys
defaultExpandedKeysreadonly string[][]Controlled / initial expanded keys
defaultExpandAllbooleanfalseExpand all nodes initially
onExpand(keys: string[], info: TreeExpandInfo) => void—Expansion changes
disabledbooleanfalseDisable interaction.
showLinebooleanfalseDisable interaction / show icons / show connecting lines
showIconbooleanfalseDisable interaction / show icons / show connecting lines
searchValuestring''Filter nodes, retaining matches and ancestors
filterTreeNode(node: TreeNode, query: string) => boolean—Custom search predicate
notFoundContentReactNodeNo optionsContent when no nodes match
loadData(node: TreeNode, signal: AbortSignal) => Promise<readonly TreeNode[]>—Load children on expansion; signal supports cancellation
cacheKeystring | number—Changing the data version clears lazy caches and cancels stale requests.
nodeRender(node: TreeNode) => ReactNode—Customize node content.
onLoad(node: TreeNode, children: readonly TreeNode[]) => void—Children loaded
onLoadError(error: unknown, node: TreeNode) => void—Loading failed; expand again to retry
virtualbooleanfalseRender visible nodes only; enable for large trees.
heightnumber320Height, numbers are in pixels
itemHeightnumber42Estimated virtual row height including spacing; follows the regional height and density. Actual rows are measured.
draggableboolean | ((node: TreeNode) => boolean)falseEnable node drag handles
allowDrop(info: Omit<TreeDropInfo, 'data'>) => boolean—Restrict drop positions
onDrop(info: TreeDropInfo) => void—Drop result; update data in the caller

TreeNode

PropertyTypeDefaultDescription
keystring—Required. Unique key, required
titleReactNode—Required. Node content, required
childrenreadonly TreeNode[]—Child nodes using this same structure
iconReactNode—Custom icon
disabledboolean—Disable interaction.
selectableboolean—Allow selection / display a checkbox; enabled by default
checkableboolean—Allow selection / display a checkbox; enabled by default
disableCheckboxboolean—Disable checks and stop check propagation
searchLabelstring—Search and accessible text for non-text titles
isLeafboolean—false allows asynchronously loaded children

TreeSelectInfo

PropertyTypeDefaultDescription
nodeTreeNode—Required. Operated node
selectedboolean—Required. Whether the node is now selected
selectedNodesreadonly TreeNode[]—Required. All selected nodes

TreeCheckInfo

PropertyTypeDefaultDescription
nodeTreeNode—Required. Operated node
checkedboolean—Required. Whether the node is now checked
checkedNodesreadonly TreeNode[]—Required. All fully checked nodes
halfCheckedKeysreadonly string[]—Required. Partially checked keys

TreeExpandInfo

PropertyTypeDefaultDescription
nodeTreeNode—Required. Operated node
expandedboolean—Required. Whether the node is now expanded

Types

Import these types to define props or prepare data.

import type { TreeProps, TreeNode, TreeSelectInfo, TreeCheckInfo, TreeExpandInfo } from '@ifhover/leaf-ui';

Usage notes

Each node needs a unique, stable key. Example data:

import type { TreeNode } from '@ifhover/leaf-ui';
export const treeData: readonly TreeNode[] = [
  {
    key: 'design',
    title: 'Design resources',
    children: [
      { key: 'components', title: 'Component guidelines' },
      { key: 'colors', title: 'Brand colors' },
      { key: 'archive', title: 'Archived resources (disabled)', disabled: true },
    ],
  },
  {
    key: 'engineering',
    title: 'Engineering',
    children: [
      { key: 'web', title: 'Website' },
      { key: 'mobile', title: 'Mobile app' },
    ],
  },
];

Checking a parent checks enabled children; disabled nodes are unaffected. Partially checked children create a mixed parent. checkStrictly keeps checks independent. In multiple mode, clicking toggles selection; Shift selects a range of visible nodes.

Use up/down to move, right/left to expand/collapse, Home/End to jump, Enter to select and Space to check. Typing locates nodes. Search temporarily reveals matching paths; clearing it restores the original expansion.

Extension types

import type { TreeDropInfo } from '@ifhover/leaf-ui';

TreeDropInfo API

PropertyTypeDefaultDescription
nodeTreeNodeRequiredRequired. Operated node
targetTreeNodeRequiredRequired. Target node receiving the drop.
position'before' | 'inside' | 'after'RequiredRequired. Place before, inside or after the target node.
dataTreeNode[]RequiredRequired. Hierarchical data

moveTreeNode

Move a node in a custom data operation. This pure function returns a new tree without changing its input. It returns null for missing targets, self moves or moves into descendants. Tree already includes the new data in its onDrop callback.

import { moveTreeNode } from '@ifhover/leaf-ui';

const next = moveTreeNode(data, 'design', 'team', 'inside');
ParameterTypeDescription
datareadonly TreeNode[]Source tree
keystringNode key to move
targetstringTarget node key
position'before' | 'inside' | 'after'Position relative to the target
Return valueTreeNode[] | nullNew tree, or null for an invalid move