# TreeView

Import: `import { TreeView } from '@neo4j-ndl/react/next'`

## Props

### TreeView

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ariaLabel` | `string \| null` | ✅ |  | The aria-label for the tree. Required for accessibility, unless using ariaLabelledby. Pass null to omit. |
| `ariaLabelledby` | `string` |  |  | The aria-labelledby for the tree. Pass a string of space-separated IDs of elements that label the tree. |
| `children` | `ReactNode` |  |  | The children of the tree. Should be TreeView.Item components or TreeView.SkeletonItem components only. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |
| `selectionMode` | `'multiple' \| 'single'` |  | `single` | Selection mode for the tree. Defaults to "single". |

### TreeView.Item

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `actionMenuItems` | `ReactNode` |  |  | ReactNode containing Menu.Item elements to render in an action menu. When provided, an action button is rendered. |
| `actionMenuProps` | `MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>` |  |  | Props forwarded to the Menu sub-components (anchorRef, onClose). |
| `children` | `ReactNode` |  |  | Nested TreeView.Item or TreeView.SkeletonItem elements rendered as sub-items in a collapsible group. |
| `defaultExpanded` | `boolean` |  | `false` | Default expansion for uncontrolled items |
| `hasChildren` | `boolean` |  |  | Whether the item has children. Decides if the item should render a chevron for expansion. Needed for lazy loading. |
| `isDisabled` | `boolean` |  |  | Whether the item is disabled |
| `isExpanded` | `boolean` |  |  | Whether the item is expanded. Makes the item controlled. |
| `isIndeterminate` | `boolean` |  |  | Whether the item is in an indeterminate state. Only meaningful in multi-select mode for parent nodes. |
| `isLoading` | `boolean` |  |  | Whether the item is loading. Applies aria-busy="true" to the item. |
| `isSelected` | `boolean` |  |  | Whether the item is selected |
| `leadingVisual` | `ReactNode` |  |  | Leading visual for the item. |
| `onExpandedChange` | `(isExpanded: boolean) => void` |  |  | Callback called when the item is expanded/collapsed. |
| `onSelectedChange` | `(isSelected: boolean) => void` |  |  | Callback called when the item is selected/deselected. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |
| `title` | `ReactNode` |  |  | The label content displayed in the item row. |
| `tooltipContent` | `ReactNode` |  |  | Content rendered inside the tooltip. When provided, the item is wrapped in a Tooltip. |
| `tooltipProps` | `TooltipObjectProps` |  |  | Props forwarded to the Tooltip sub-components (root, trigger, content). |
| `trailingContent` | `ReactNode` |  |  | Trailing content for the item. Shown after the children but before the action menu. |

### TreeView.SkeletonItem

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |
| `rows` | `number` |  | `1` | The number of rows to render. |

## Accessibility

## Keyboard interactions

Implements the keyboard interactions defined in the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).

| Key | Description |
|-----|-------------|
| `ArrowDown` | Moves focus to the next visible tree item |
| `ArrowUp` | Moves focus to the previous visible tree item |
| `ArrowRight` | Expands a collapsed parent item. If already expanded, moves focus to the first child item |
| `ArrowLeft` | Collapses an expanded parent item. If already collapsed (or a leaf), moves focus to the parent item |
| `Home` | Moves focus to the first tree item |
| `End` | Moves focus to the last visible tree item |
| `Enter` | Toggles selection on the focused item, or toggles expansion if the item is not selectable |
| `Space` | Toggles selection on the focused item, or toggles expansion if the item is not selectable |
| `Shift + F10` | Opens the action menu on the focused item (when `actionMenu` is provided) |

The tree uses a roving tabindex strategy: only the currently focused item has `tabIndex={0}`, all other items have `tabIndex={-1}`. When the tree root receives focus it delegates to the previously focused item, or the first item if none was focused.

## WAI-ARIA roles and attributes

The TreeView component follows the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).

- The root container has `role="tree"` and is labeled via `aria-label` or `aria-labelledby`
- When `selectionMode` is `"multiple"`, the root sets `aria-multiselectable="true"`
- Each item has `role="treeitem"` with `aria-level`, `aria-posinset`, and `aria-setsize` set automatically
- Parent items (items with children) set `aria-expanded` and `aria-owns` linking to their child `role="group"` container
- In single selection mode, selected items set `aria-selected`. In multiple selection mode, selected items set `aria-checked`, with `aria-checked="mixed"` for indeterminate parent nodes
- Disabled items set `aria-disabled="true"`
- Loading items set `aria-busy="true"`
- When `actionMenu` is provided, the treeitem sets `aria-keyshortcuts="Shift+F10"` and the action button sets `aria-haspopup="menu"`, `aria-expanded`, and `aria-label="Actions"`
- `TreeView.SkeletonItem` is `aria-hidden="true"` since it is a visual placeholder with no interactive content
- Child groups use `role="group"` and are `hidden` when the parent is collapsed

## Implementation guidelines

- Always provide either `ariaLabel` or `ariaLabelledby` on the root so the tree has an accessible name
- `role="tree"` only allows children with roles `treeitem` or `group`. The built-in sub-components handle this automatically, but wrapping items in custom elements without a valid role will break the tree semantics for assistive technologies
- In single selection mode (`selectionMode="single"`, the default), only one item should have `isSelected` set to `true` at a time. The component does not enforce this — it is the consumer's responsibility to manage the selection state
- In multiple selection mode, manage `isIndeterminate` on parent nodes to communicate partial selection via `aria-checked="mixed"`. Screen reader users rely on this to understand that some but not all children are selected
- When using `hasChildren` for lazy loading, set `isLoading` to apply `aria-busy="true"` and render `TreeView.SkeletonItem` inside the expanded item so users know content is being fetched
- The checkbox rendered in multiple selection mode has `tabIndex={-1}` — selection is driven from the treeitem via `Enter`/`Space`, not from the checkbox directly
- The action menu button has `tabIndex={-1}` and is only reachable via `Shift+F10`, keeping arrow-key navigation within the tree clean

### Related WCAG criteria

- [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (A): All tree items are fully navigable and operable via keyboard
- [2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) (A): Roving tabindex ensures a logical focus order; `ArrowLeft` returns focus to the parent item
- [1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) (A): Tree hierarchy is expressed via `aria-level`, `aria-posinset`, `aria-setsize`, and `role="group"` nesting
- [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (A): Roles (`tree`, `treeitem`, `group`), states (`aria-expanded`, `aria-selected`/`aria-checked`, `aria-disabled`, `aria-busy`), and accessible names (`aria-label`/`aria-labelledby`) are set automatically by the component

## Examples

### Actions

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { Menu } from '@neo4j-ndl/react';
import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';

const Component = () => {
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
  const [selected, setSelected] = useState<string | null>(null);

  const toggle = (key: string) =>
    setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));

  return (
    <TreeView ariaLabel="Project files">
      <TreeView.Item
        title="src"
        hasChildren
        isExpanded={expanded['src']}
        onExpandedChange={() => toggle('src')}
        isSelected={selected === 'src'}
        onSelectedChange={() => setSelected('src')}
        leadingVisual={<FolderIconSolid />}
        actionMenuItems={
          <>
            <Menu.Item
              title="New File"
              onClick={() => alert('New file in src')}
            />
            <Menu.Item title="Rename" onClick={() => alert('Rename src')} />
            <Menu.Item title="Delete" onClick={() => alert('Delete src')} />
          </>
        }
      >
        <TreeView.Item
          title="index.ts"
          isSelected={selected === 'index'}
          onSelectedChange={() => setSelected('index')}
          leadingVisual={<DocumentIconOutline />}
          actionMenuItems={
            <>
              <Menu.Item title="Open" onClick={() => alert('Open index.ts')} />
              <Menu.Item
                title="Rename"
                onClick={() => alert('Rename index.ts')}
              />
              <Menu.Item
                title="Delete"
                onClick={() => alert('Delete index.ts')}
              />
            </>
          }
        />
        <TreeView.Item
          title="utils.ts"
          isSelected={selected === 'utils'}
          onSelectedChange={() => setSelected('utils')}
          leadingVisual={<DocumentIconOutline />}
          actionMenuItems={
            <>
              <Menu.Item title="Open" onClick={() => alert('Open utils.ts')} />
              <Menu.Item
                title="Rename"
                onClick={() => alert('Rename utils.ts')}
              />
              <Menu.Item
                title="Delete"
                onClick={() => alert('Delete utils.ts')}
              />
            </>
          }
        />
      </TreeView.Item>
      <TreeView.Item
        title="tests"
        hasChildren
        isExpanded={expanded['tests']}
        onExpandedChange={() => toggle('tests')}
        isSelected={selected === 'tests'}
        onSelectedChange={() => setSelected('tests')}
        leadingVisual={<FolderIconSolid />}
        actionMenuItems={
          <>
            <Menu.Item
              title="Run Tests"
              onClick={() => alert('Run all tests')}
            />
            <Menu.Item
              title="New Test"
              onClick={() => alert('New test in tests')}
            />
            <Menu.Item title="Delete" onClick={() => alert('Delete tests')} />
          </>
        }
      >
        <TreeView.Item
          title="index.test.ts"
          isSelected={selected === 'index-test'}
          onSelectedChange={() => setSelected('index-test')}
          leadingVisual={<DocumentIconOutline />}
          actionMenuItems={
            <>
              <Menu.Item
                title="Run"
                onClick={() => alert('Run index.test.ts')}
              />
              <Menu.Item
                title="Rename"
                onClick={() => alert('Rename index.test.ts')}
              />
              <Menu.Item
                title="Delete"
                onClick={() => alert('Delete index.test.ts')}
              />
            </>
          }
        />
        <TreeView.Item
          title="utils.test.ts"
          isSelected={selected === 'utils-test'}
          onSelectedChange={() => setSelected('utils-test')}
          leadingVisual={<DocumentIconOutline />}
          actionMenuItems={
            <>
              <Menu.Item
                title="Run"
                onClick={() => alert('Run utils.test.ts')}
              />
              <Menu.Item
                title="Rename"
                onClick={() => alert('Rename utils.test.ts')}
              />
              <Menu.Item
                title="Delete"
                onClick={() => alert('Delete utils.test.ts')}
              />
            </>
          }
        />
      </TreeView.Item>
      <TreeView.Item
        title="README.md"
        isSelected={selected === 'readme'}
        onSelectedChange={() => setSelected('readme')}
        leadingVisual={<DocumentIconOutline />}
        actionMenuItems={
          <>
            <Menu.Item title="Open" onClick={() => alert('Open README.md')} />
            <Menu.Item
              title="Rename"
              onClick={() => alert('Rename README.md')}
            />
            <Menu.Item
              title="Delete"
              onClick={() => alert('Delete README.md')}
            />
          </>
        }
      />
    </TreeView>
  );
};

export default Component;
```

### Lazy

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import {
  DocumentIconOutline,
  FolderIconSolid,
  PhotoIconOutline,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useCallback, useMemo, useRef, useState } from 'react';

type TreeNode = {
  id: string;
  label: string;
  children?: TreeNode[];
};

const TREE_DATA: TreeNode[] = [
  {
    children: [
      {
        children: [
          { id: 'app-tsx', label: 'App.tsx' },
          { id: 'index-tsx', label: 'index.tsx' },
          { id: 'styles-css', label: 'styles.css' },
        ],
        id: 'frontend',
        label: 'Frontend',
      },
      {
        children: [
          { id: 'server-ts', label: 'server.ts' },
          { id: 'routes-ts', label: 'routes.ts' },
        ],
        id: 'backend',
        label: 'Backend',
      },
      { id: 'readme-md', label: 'README.md' },
    ],
    id: 'projects',
    label: 'Projects',
  },
  {
    children: [
      {
        children: [
          { id: 'beach-jpg', label: 'beach.jpg' },
          { id: 'sunset-jpg', label: 'sunset.jpg' },
        ],
        id: 'vacation',
        label: 'Vacation',
      },
      { id: 'profile-png', label: 'profile.png' },
    ],
    id: 'photos',
    label: 'Photos',
  },
  {
    children: [
      { id: 'resume-pdf', label: 'resume.pdf' },
      { id: 'cover-letter-pdf', label: 'cover-letter.pdf' },
      { id: 'notes-txt', label: 'notes.txt' },
    ],
    id: 'documents',
    label: 'Documents',
  },
  {
    children: [
      {
        children: [
          { id: 'chill-m3u', label: 'chill.m3u' },
          { id: 'workout-m3u', label: 'workout.m3u' },
        ],
        id: 'playlists',
        label: 'Playlists',
      },
      { id: 'favorites-m3u', label: 'favorites.m3u' },
    ],
    id: 'music',
    label: 'Music',
  },
  { id: 'todo-txt', label: 'TODO.txt' },
];

const LOAD_DELAY_MS = 600;

function iconForNode(node: TreeNode) {
  if (node.children) {
    return <FolderIconSolid />;
  }
  if (/\.(jpg|jpeg|png|gif|svg)$/i.test(node.label)) {
    return <PhotoIconOutline />;
  }
  return <DocumentIconOutline />;
}

type NodeState = {
  isExpanded: boolean;
  isLoading: boolean;
  hasLoaded: boolean;
};

const INITIAL_NODE_STATE: NodeState = {
  hasLoaded: false,
  isExpanded: false,
  isLoading: false,
};

function useTreeState() {
  const [nodeStates, setNodeStates] = useState<Record<string, NodeState>>({});
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const timers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});

  const getNodeState = useCallback(
    (id: string): NodeState => nodeStates[id] ?? INITIAL_NODE_STATE,
    [nodeStates],
  );

  const handleExpand = useCallback((id: string, expanded: boolean) => {
    setNodeStates((prev) => {
      const current = prev[id] ?? INITIAL_NODE_STATE;
      if (expanded && !current.hasLoaded) {
        if (timers.current[id]) {
          clearTimeout(timers.current[id]);
        }
        timers.current[id] = setTimeout(() => {
          setNodeStates((p) => ({
            ...p,
            [id]: { hasLoaded: true, isExpanded: true, isLoading: false },
          }));
          delete timers.current[id];
        }, LOAD_DELAY_MS);
        return {
          ...prev,
          [id]: { hasLoaded: false, isExpanded: true, isLoading: true },
        };
      }
      return { ...prev, [id]: { ...current, isExpanded: expanded } };
    });
  }, []);

  const handleSelect = useCallback(
    (id: string) => setSelectedId((prev) => (prev === id ? null : id)),
    [],
  );

  return { getNodeState, handleExpand, handleSelect, selectedId };
}

type RenderContext = {
  getNodeState: (id: string) => NodeState;
  selectedId: string | null;
  handleExpand: (id: string, expanded: boolean) => void;
  handleSelect: (id: string) => void;
};

function renderNode(node: TreeNode, ctx: RenderContext) {
  const hasChildren =
    node.children !== undefined &&
    node.children !== null &&
    node.children.length > 0;
  const state = ctx.getNodeState(node.id);

  return (
    <TreeView.Item
      key={node.id}
      title={node.label}
      hasChildren={hasChildren}
      isExpanded={state.isExpanded}
      isLoading={state.isLoading}
      isSelected={ctx.selectedId === node.id}
      leadingVisual={iconForNode(node)}
      onExpandedChange={(expanded: boolean) =>
        ctx.handleExpand(node.id, expanded)
      }
      onSelectedChange={() => ctx.handleSelect(node.id)}
    >
      {hasChildren && state.isLoading && !state.hasLoaded && (
        <TreeView.SkeletonItem rows={2} />
      )}
      {hasChildren &&
        state.hasLoaded &&
        node.children!.map((child) => renderNode(child, ctx))}
    </TreeView.Item>
  );
}

const Component = () => {
  const { getNodeState, selectedId, handleExpand, handleSelect } =
    useTreeState();

  const ctx = useMemo<RenderContext>(
    () => ({ getNodeState, handleExpand, handleSelect, selectedId }),
    [getNodeState, selectedId, handleExpand, handleSelect],
  );

  return (
    <TreeView ariaLabel="Lazy-loaded file explorer">
      {TREE_DATA.map((node) => renderNode(node, ctx))}
    </TreeView>
  );
};

export default Component;
```

### Multi

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import {
  DocumentIconOutline,
  FolderIconSolid,
  PhotoIconOutline,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useCallback, useState } from 'react';

type CheckedState = Record<string, boolean>;

const WORK_CHILDREN = ['report', 'presentation'] as const;
const DOC_CHILDREN = ['work', 'resume'] as const;
const IMAGE_CHILDREN = ['photo', 'screenshot'] as const;
const DISABLED_KEYS = new Set(['presentation']);

function getParentChecked(
  checked: CheckedState,
  childKeys: readonly string[],
): { isChecked: boolean; isIndeterminate: boolean } {
  const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k));
  if (enabledKeys.length === 0) {
    return { isChecked: false, isIndeterminate: false };
  }
  const checkedCount = enabledKeys.filter((k) => checked[k]).length;
  if (checkedCount === 0) {
    return { isChecked: false, isIndeterminate: false };
  }
  if (checkedCount === enabledKeys.length) {
    return { isChecked: true, isIndeterminate: false };
  }
  return { isChecked: false, isIndeterminate: true };
}

const Component = () => {
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
  const [checked, setChecked] = useState<CheckedState>({});

  const toggle = (key: string) =>
    setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));

  const toggleCheck = useCallback((key: string) => {
    setChecked((prev) => ({ ...prev, [key]: !prev[key] }));
  }, []);

  const toggleParent = useCallback(
    (childKeys: readonly string[]) => {
      const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k));
      const isAllChecked = enabledKeys.every((k) => checked[k]);
      setChecked((prev) => {
        const next = { ...prev };
        for (const k of enabledKeys) {
          next[k] = !isAllChecked;
        }
        return next;
      });
    },
    [checked],
  );

  const docState = getParentChecked(checked, [
    ...DOC_CHILDREN,
    ...WORK_CHILDREN,
  ]);
  const workState = getParentChecked(checked, WORK_CHILDREN);
  const imageState = getParentChecked(checked, IMAGE_CHILDREN);

  return (
    <TreeView ariaLabel="File explorer" selectionMode="multiple">
      <TreeView.Item
        title="Documents"
        hasChildren
        isExpanded={expanded['documents']}
        onExpandedChange={() => toggle('documents')}
        isSelected={docState.isChecked}
        isIndeterminate={docState.isIndeterminate}
        onSelectedChange={() =>
          toggleParent([...DOC_CHILDREN, ...WORK_CHILDREN])
        }
        leadingVisual={<FolderIconSolid />}
      >
        <TreeView.Item
          title="Work"
          hasChildren
          isExpanded={expanded['work']}
          onExpandedChange={() => toggle('work')}
          isSelected={workState.isChecked}
          isIndeterminate={workState.isIndeterminate}
          onSelectedChange={() => toggleParent(WORK_CHILDREN)}
          leadingVisual={<FolderIconSolid />}
        >
          <TreeView.Item
            title="Report.pdf"
            isSelected={!!checked['report']}
            onSelectedChange={() => toggleCheck('report')}
            leadingVisual={<DocumentIconOutline />}
          />
          <TreeView.Item
            title="Presentation.pptx"
            isDisabled
            onSelectedChange={() => null}
            leadingVisual={<DocumentIconOutline />}
          />
        </TreeView.Item>
        <TreeView.Item
          title="Resume.pdf"
          isSelected={!!checked['resume']}
          onSelectedChange={() => toggleCheck('resume')}
          leadingVisual={<DocumentIconOutline />}
        />
      </TreeView.Item>
      <TreeView.Item
        title="Images"
        hasChildren
        isExpanded={expanded['images']}
        onExpandedChange={() => toggle('images')}
        isSelected={imageState.isChecked}
        isIndeterminate={imageState.isIndeterminate}
        onSelectedChange={() => toggleParent(IMAGE_CHILDREN)}
        leadingVisual={<FolderIconSolid />}
      >
        <TreeView.Item
          title="Photo.jpg"
          isSelected={!!checked['photo']}
          onSelectedChange={() => toggleCheck('photo')}
          leadingVisual={<PhotoIconOutline />}
        />
        <TreeView.Item
          title="Screenshot.png"
          isSelected={!!checked['screenshot']}
          onSelectedChange={() => toggleCheck('screenshot')}
          leadingVisual={<PhotoIconOutline />}
        />
      </TreeView.Item>
    </TreeView>
  );
};

export default Component;
```

### Non Selectable Folders

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';

const Component = () => {
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
  const [selected, setSelected] = useState<string | null>(null);

  const toggle = (key: string) =>
    setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));

  return (
    <TreeView ariaLabel="File explorer">
      <TreeView.Item
        title="Documents"
        hasChildren
        isExpanded={expanded['documents']}
        onExpandedChange={() => toggle('documents')}
        leadingVisual={<FolderIconSolid />}
      >
        <TreeView.Item
          title="Work"
          hasChildren
          isExpanded={expanded['work']}
          onExpandedChange={() => toggle('work')}
          leadingVisual={<FolderIconSolid />}
        >
          <TreeView.Item
            title="Report.pdf"
            isSelected={selected === 'report'}
            onSelectedChange={() => setSelected('report')}
            leadingVisual={<DocumentIconOutline />}
          />
          <TreeView.Item
            title="Presentation.pptx"
            isSelected={selected === 'presentation'}
            onSelectedChange={() => setSelected('presentation')}
            leadingVisual={<DocumentIconOutline />}
          />
        </TreeView.Item>
        <TreeView.Item
          title="Resume.pdf"
          isSelected={selected === 'resume'}
          onSelectedChange={() => setSelected('resume')}
          leadingVisual={<DocumentIconOutline />}
        />
      </TreeView.Item>
      <TreeView.Item
        title="Images"
        hasChildren
        isExpanded={expanded['images']}
        onExpandedChange={() => toggle('images')}
        leadingVisual={<FolderIconSolid />}
      >
        <TreeView.Item
          title="Photo.jpg"
          isSelected={selected === 'photo'}
          onSelectedChange={() => setSelected('photo')}
          leadingVisual={<DocumentIconOutline />}
        />
        <TreeView.Item
          title="Screenshot.png"
          isSelected={selected === 'screenshot'}
          onSelectedChange={() => setSelected('screenshot')}
          leadingVisual={<DocumentIconOutline />}
        />
      </TreeView.Item>
    </TreeView>
  );
};

export default Component;
```

### Single

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';

const Component = () => {
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
  const [selected, setSelected] = useState<string | null>(null);

  const toggle = (key: string) =>
    setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));

  return (
    <TreeView ariaLabel="File explorer">
      <TreeView.Item
        title="Documents"
        hasChildren
        isExpanded={expanded['documents']}
        onExpandedChange={() => toggle('documents')}
        isSelected={selected === 'documents'}
        onSelectedChange={() => setSelected('documents')}
        leadingVisual={<FolderIconSolid />}
      >
        <TreeView.Item
          title="Work"
          hasChildren
          isExpanded={expanded['work']}
          onExpandedChange={() => toggle('work')}
          isSelected={selected === 'work'}
          onSelectedChange={() => setSelected('work')}
          leadingVisual={<FolderIconSolid />}
        >
          <TreeView.Item
            title="Report.pdf"
            isSelected={selected === 'report'}
            onSelectedChange={() => setSelected('report')}
            leadingVisual={<DocumentIconOutline />}
          />
          <TreeView.Item
            title="Presentation.pptx"
            isDisabled
            isSelected={selected === 'presentation'}
            onSelectedChange={() => setSelected('presentation')}
            leadingVisual={<DocumentIconOutline />}
          />
        </TreeView.Item>
        <TreeView.Item
          title="Resume.pdf"
          isSelected={selected === 'resume'}
          onSelectedChange={() => setSelected('resume')}
          leadingVisual={<DocumentIconOutline />}
        />
      </TreeView.Item>
      <TreeView.Item
        title="Images (disabled)"
        hasChildren
        isExpanded={expanded['images']}
        onExpandedChange={() => toggle('images')}
        isSelected={selected === 'images'}
        onSelectedChange={() => setSelected('images')}
        leadingVisual={<FolderIconSolid />}
        isDisabled
      />
      <TreeView.Item
        title="README.md"
        isSelected={selected === 'readme'}
        onSelectedChange={() => setSelected('readme')}
        leadingVisual={<DocumentIconOutline />}
      />
    </TreeView>
  );
};

export default Component;
```

### Tooltip

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { Menu } from '@neo4j-ndl/react';
import {
  AppearanceIcon,
  CheckCircleIconSolid,
  DatabaseSignalIcon,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';

const Component = () => {
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <TreeView ariaLabel="Databases" style={{ marginTop: '100px' }}>
      <TreeView.Item
        title="Production"
        isSelected={selected === 'production'}
        onSelectedChange={() => setSelected('production')}
        leadingVisual={<DatabaseSignalIcon />}
        trailingContent={
          <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
        }
        actionMenuItems={
          <>
            <Menu.Item title="Edit" />
            <Menu.Item title="Delete" />
            <Menu.Item title="Rename" />
          </>
        }
        tooltipContent={
          <>
            <span className="n-font-semibold">Production</span>
            <span className="n-flex n-items-center n-gap-token-4 n-mt-token-8">
              <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
              : Endorsed
            </span>
          </>
        }
      />
      <TreeView.Item
        title="Development"
        isSelected={selected === 'development'}
        onSelectedChange={() => setSelected('development')}
        leadingVisual={<DatabaseSignalIcon />}
        trailingContent={
          <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />
        }
        tooltipContent={
          <>
            <span className="n-font-semibold">Development</span>
            <span className="n-flex n-items-center n-gap-token-4 n-mt-token-8">
              <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />:
              In development
            </span>
          </>
        }
      />
    </TreeView>
  );
};

export default Component;
```

### Trailing

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { Menu } from '@neo4j-ndl/react';
import {
  AppearanceIcon,
  CheckCircleIconSolid,
  DatabaseSignalIcon,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';

const Component = () => {
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <TreeView ariaLabel="Databases">
      <TreeView.Item
        title="Production"
        isSelected={selected === 'production'}
        onSelectedChange={() => setSelected('production')}
        leadingVisual={<DatabaseSignalIcon />}
        trailingContent={
          <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
        }
        actionMenuItems={
          <>
            <Menu.Item title="Edit" />
            <Menu.Item title="Delete" />
            <Menu.Item title="Rename" />
          </>
        }
      />
      <TreeView.Item
        title="Development"
        isSelected={selected === 'development'}
        onSelectedChange={() => setSelected('development')}
        leadingVisual={<DatabaseSignalIcon />}
        trailingContent={
          <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />
        }
      />
    </TreeView>
  );
};

export default Component;
```

### Uncontrolled

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';

const Component = () => {
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <TreeView ariaLabel="File explorer">
      <TreeView.Item
        title="Documents (expanded by default)"
        defaultExpanded={true}
        leadingVisual={<FolderIconSolid />}
      >
        <TreeView.Item title="Work" leadingVisual={<FolderIconSolid />}>
          <TreeView.Item
            title="Report.pdf"
            isSelected={selected === 'report'}
            onSelectedChange={() => setSelected('report')}
            leadingVisual={<DocumentIconOutline />}
          />
          <TreeView.Item
            title="Presentation.pptx"
            isSelected={selected === 'presentation'}
            onSelectedChange={() => setSelected('presentation')}
            leadingVisual={<DocumentIconOutline />}
          />
        </TreeView.Item>
        <TreeView.Item
          title="Resume.pdf"
          isSelected={selected === 'resume'}
          onSelectedChange={() => setSelected('resume')}
          leadingVisual={<DocumentIconOutline />}
        />
      </TreeView.Item>
      <TreeView.Item
        title="Images (collapsed by default)"
        leadingVisual={<FolderIconSolid />}
        defaultExpanded={false}
      >
        <TreeView.Item
          title="Photo.jpg"
          isSelected={selected === 'photo'}
          onSelectedChange={() => setSelected('photo')}
          leadingVisual={<DocumentIconOutline />}
        />
        <TreeView.Item
          title="Screenshot.png"
          isSelected={selected === 'screenshot'}
          onSelectedChange={() => setSelected('screenshot')}
          leadingVisual={<DocumentIconOutline />}
        />
      </TreeView.Item>
    </TreeView>
  );
};

export default Component;
```
