> For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt.

# TreeView

The interactive hierarchy: rows that expand, select and disable, with the connector rails drawn for you. Reach for `Tree` when you only need to _show_ nesting — this one is for working in it. Rows are composed freely, so an icon, a badge or a checkbox is just a child, and nested `TreeViewItem`s become the subtree.

[Source](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/TreeView) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/navigation-treeview--overview)

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { Input } from '@wallarm-org/design-system/Input';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const Basic = () => (
  <div className='w-320'>
    <TreeView>
      <TreeViewItem defaultOpen>
        <Folder />
        Components
        <TreeViewItem>
          <FileText />
          Button.tsx
        </TreeViewItem>
        <TreeViewItem>
          <FileText />
          Input.tsx
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem>
        <Folder />
        Utils
        <TreeViewItem>
          <FileText />
          cn.ts
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem>
        <FileText />
        index.ts
      </TreeViewItem>
    </TreeView>
  </div>
);
```

## Usage

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { Input } from '@wallarm-org/design-system/Input';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';
```

## Examples

### Nested

Nested items become the collapsible subtree, which is also what marks the parent as a branch. Branches start closed.

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const Nested = () => (
  <div className='w-320'>
    <TreeView>
      <TreeViewItem defaultOpen>
        <Folder />
        src
        <TreeViewItem defaultOpen>
          <Folder />
          components
          <TreeViewItem defaultOpen>
            <Folder />
            TreeView
            <TreeViewItem>
              <FileText />
              TreeView.tsx
            </TreeViewItem>
          </TreeViewItem>
          <TreeViewItem>
            <FileText />
            Button.tsx
          </TreeViewItem>
        </TreeViewItem>
        <TreeViewItem>
          <FileText />
          index.ts
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem>
        <Folder />
        public
        <TreeViewItem>
          <FileText />
          favicon.ico
        </TreeViewItem>
      </TreeViewItem>
    </TreeView>
  </div>
);
```

### With checkboxes

The tree has no checkbox logic: the checkbox is a child, the checked set is yours, and the click has to be stopped from reaching row selection.

```tsx
import { useState } from 'react';
import { Checkbox, CheckboxIndicator } from '@wallarm-org/design-system/Checkbox';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const WithCheckboxes = () => {
  const [checked, setChecked] = useState<Set<string>>(() => new Set(['users']));
  const cb = (id: string) => (
    <Checkbox
      checked={checked.has(id)}
      onClick={e => e.stopPropagation()}
      onCheckedChange={details =>
        setChecked(prev => {
          const next = new Set(prev);
          if (details.checked === true) {
            next.add(id);
          } else {
            next.delete(id);
          }
          return next;
        })
      }
      className='shrink-0'
    >
      <CheckboxIndicator />
    </Checkbox>
  );

  return (
    <div className='w-320'>
      <TreeView>
        <TreeViewItem defaultOpen>
          {cb('endpoints')}
          <Folder />
          Endpoints
          <TreeViewItem>
            {cb('users')}
            <FileText />
            /users
          </TreeViewItem>
          <TreeViewItem>
            {cb('orders')}
            <FileText />
            /orders
          </TreeViewItem>
        </TreeViewItem>
        <TreeViewItem>
          {cb('health')}
          <FileText />
          /health
        </TreeViewItem>
      </TreeView>
    </div>
  );
};
```

### Selectable

`selectable` (with `multiSelect` for more than one) turns rows into a selection, controlled through `selectedIds` or left to the component.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { Input } from '@wallarm-org/design-system/Input';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const Selectable = () => {
  const [selected, setSelected] = useState<string[]>(['button']);
  return (
    <div className='w-320'>
      <TreeView selectable selectedIds={selected} onSelectionChange={setSelected}>
        <TreeViewItem id='components' defaultOpen>
          <Folder />
          Components
          <TreeViewItem id='button'>
            <FileText />
            Button.tsx
          </TreeViewItem>
          <TreeViewItem id='input'>
            <FileText />
            Input.tsx
          </TreeViewItem>
        </TreeViewItem>
        <TreeViewItem id='index'>
          <FileText />
          index.ts
        </TreeViewItem>
      </TreeView>
    </div>
  );
};
```

### With inline badge

A badge beside the label, for a count that belongs to the row rather than to its children.

```tsx
import { Badge } from '@wallarm-org/design-system/Badge';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const WithInlineBadge = () => (
  <div className='w-320'>
    <TreeView>
      <TreeViewItem defaultOpen>
        <Folder />
        Endpoints
        <TreeViewItem>
          <FileText />
          <Badge color='slate' size='small'>
            new
          </Badge>
          /users
        </TreeViewItem>
        <TreeViewItem>
          <FileText />
          <Badge color='green' type='secondary' size='small' textVariant='code'>
            GET
          </Badge>
          /orders
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem>
        <FileText />
        <Badge color='amber' size='small'>
          wip
        </Badge>
        /health
      </TreeViewItem>
    </TreeView>
  </div>
);
```

### With trailing content

The label is `flex-1`, so anything after it lands against the right edge — which is how a count or a tag ends up trailing without any prop for it.

```tsx
import { Badge } from '@wallarm-org/design-system/Badge';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const WithTrailingContent = () => (
  <div className='w-320'>
    <TreeView>
      <TreeViewItem defaultOpen>
        <Folder />
        src
        <CountBadge value={3} />
        <TreeViewItem>
          <FileText />
          index.ts
          <CountBadge value={12} />
        </TreeViewItem>
        <TreeViewItem>
          <FileText />
          config.ts
          <Badge color='blue' size='small'>
            label
          </Badge>
        </TreeViewItem>
        <TreeViewItem>
          <FileText />
          types.ts
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem>
        <Folder />
        public
        <CountBadge value={1} />
        <TreeViewItem>
          <FileText />
          favicon.ico
        </TreeViewItem>
      </TreeViewItem>
    </TreeView>
  </div>
);
```

### Disabled

A disabled row dims and stops responding to everything: toggle, selection and checkbox alike.

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { FileText, Folder } from '@wallarm-org/design-system/icons';
import { Input } from '@wallarm-org/design-system/Input';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const Disabled = () => (
  <div className='w-320'>
    <TreeView selectable>
      <TreeViewItem id='components' defaultOpen>
        <Folder />
        Components
        <TreeViewItem id='button'>
          <FileText />
          Button.tsx
        </TreeViewItem>
        <TreeViewItem id='input' disabled>
          <FileText />
          Input.tsx (disabled)
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem disabled>
        <Folder />
        Utils (disabled)
        <TreeViewItem>
          <FileText />
          cn.ts
        </TreeViewItem>
      </TreeViewItem>
      <TreeViewItem id='index'>
        <FileText />
        index.ts
      </TreeViewItem>
    </TreeView>
  </div>
);
```

### With header and search

The toolbar is assembled from `Text`, `Button` and `Input` — there is no TreeView toolbar. Expand-all drives the items’ controlled `open` state, and the search keeps ancestors of matches and opens them.

```tsx
import { type ReactNode, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { ChevronsDownUp, ChevronsUpDown, FileText, Folder, PanelRightClose, Search } from '@wallarm-org/design-system/icons';
import { Input } from '@wallarm-org/design-system/Input';
import { Text } from '@wallarm-org/design-system/Text';
import { TreeView, TreeViewItem } from '@wallarm-org/design-system/TreeView';

export const WithHeaderAndSearch = () => {
  const [query, setQuery] = useState('');
  const q = query.trim().toLowerCase();
  const [openIds, setOpenIds] = useState<Set<string>>(() => new Set(['src', 'components']));

  const isOpen = (id: string) => (q ? true : openIds.has(id));
  const toggle = (id: string, v: boolean) =>
    setOpenIds(prev => {
      const next = new Set(prev);
      if (v) {
        next.add(id);
      } else {
        next.delete(id);
      }
      return next;
    });
  const setAll = (open: boolean) => setOpenIds(open ? new Set(BRANCH_IDS) : new Set());

  const renderNode = (node: SearchNode): ReactNode => {
    const selfMatch = !q || node.label.toLowerCase().includes(q);
    const icon = node.type === 'folder' ? <Folder /> : <FileText />;

    if (node.children) {
      const kids = node.children.map(renderNode).filter(Boolean);
      if (q && !selfMatch && kids.length === 0) {
        return null;
      }
      return (
        <TreeViewItem key={node.id} open={isOpen(node.id)} onOpenChange={v => toggle(node.id, v)}>
          {icon}
          {node.label}
          {kids}
        </TreeViewItem>
      );
    }

    if (!selfMatch) {
      return null;
    }
    return (
      <TreeViewItem key={node.id}>
        {icon}
        {node.label}
      </TreeViewItem>
    );
  };

  const nodes = TREE_DATA.map(renderNode).filter(Boolean);

  return (
    <div className='w-320'>
      <TreeView data-testid='tree-view'>
        {/* Header — expand/collapse buttons grouped tightly, close set apart */}
        <div className='flex items-center gap-8 px-8 pt-8 pb-4'>
          <Text size='xs' weight='medium' color='secondary' grow truncate>
            Header
          </Text>
          <div className='flex shrink-0 items-center gap-8'>
            <div className='flex items-center gap-2'>
              <Button
                variant='ghost'
                color='neutral'
                size='small'
                aria-label='Collapse all'
                onClick={() => setAll(false)}
              >
                <ChevronsDownUp size='sm' />
              </Button>
              <Button
                variant='ghost'
                color='neutral'
                size='small'
                aria-label='Expand all'
                onClick={() => setAll(true)}
              >
                <ChevronsUpDown size='sm' />
              </Button>
            </div>
            <Button variant='ghost' color='neutral' size='small' aria-label='Close panel'>
              <PanelRightClose size='sm' />
            </Button>
          </div>
        </div>

        {/* Search — full width, aligned with the items' selection highlight */}
        <div className='pb-4'>
          <div className='relative'>
            <Search
              size='md'
              className='pointer-events-none absolute top-1/2 left-12 -translate-y-1/2 text-text-secondary'
            />
            <Input
              placeholder='Search'
              value={query}
              onChange={e => setQuery(e.target.value)}
              className='pl-40'
            />
          </div>
        </div>

        {nodes}
      </TreeView>
    </div>
  );
};
```

## Props

| Prop               | Type                                                 | Default | Description                                       |
| ------------------ | ---------------------------------------------------- | ------- | ------------------------------------------------- |
| selectable         | `boolean \| undefined`                               | `false` | Allow rows to be selected by clicking.            |
| multiSelect        | `boolean \| undefined`                               | `false` | Allow more than one row to be selected at a time. |
| selectedIds        | `string[] \| undefined`                              |         | Controlled selected item ids.                     |
| defaultSelectedIds | `string[] \| undefined`                              |         | Uncontrolled initial selected item ids.           |
| onSelectionChange  | `((ids: string[]) => void) \| undefined`             |         | Called whenever the selection changes.            |
| defaultChecked     | `boolean \| undefined`                               |         |                                                   |
| defaultValue       | `string \| number \| readonly string[] \| undefined` |         |                                                   |
| autoFocus          | `boolean \| undefined`                               |         |                                                   |
| id                 | `string \| undefined`                                |         |                                                   |
| tabIndex           | `number \| undefined`                                |         |                                                   |
| title              | `string \| undefined`                                |         |                                                   |
| rel                | `string \| undefined`                                |         |                                                   |
| onChange           | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                   |
| data-testid        | `string \| undefined`                                |         |                                                   |

### TreeViewItem

| Prop           | Type                                                 | Default | Description                                                                 |
| -------------- | ---------------------------------------------------- | ------- | --------------------------------------------------------------------------- |
| id             | `string \| undefined`                                |         | Stable id used for selection. Required for a row to be selectable.          |
| expandable     | `boolean \| undefined`                               |         | Force branch behaviour (show a toggle) even without nested items.           |
| open           | `boolean \| undefined`                               |         | Controlled open state.                                                      |
| defaultOpen    | `boolean \| undefined`                               | `false` | Uncontrolled initial open state.                                            |
| onOpenChange   | `((open: boolean) => void) \| undefined`             |         |                                                                             |
| selected       | `boolean \| undefined`                               |         | Visually mark the row as selected (defaults to the tree's selection state). |
| disabled       | `boolean \| undefined`                               | `false` | Dim the row and disable all interaction (toggle, selection, checkbox).      |
| title          | `string \| undefined`                                |         |                                                                             |
| defaultChecked | `boolean \| undefined`                               |         |                                                                             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                             |
| autoFocus      | `boolean \| undefined`                               |         |                                                                             |
| tabIndex       | `number \| undefined`                                |         |                                                                             |
| rel            | `string \| undefined`                                |         |                                                                             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                             |
