# Tree View

> A React tree view for folders, categories or permissions: animated expand, selection, tri-state checkboxes, filter and drag to move.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add tree-view`
- Docs: https://wingo-ui.com/components/tree-view

## Interaction

Click a row to select it; clicking a folder also opens or closes it while its children slide in and the chevron turns on a quick spring, and the chevron alone toggles too. Double click a file (or tap it on a phone) to open it. In multiple mode clicks toggle rows and Shift + click selects a range. With checkboxes, clicking a box checks it and, with cascade, everything under it; parents turn to a dash when only some children are checked. With selectionMode none a click on a file row checks it too, while a click on a folder row only opens it. Keyboard (one Tab stop): ArrowDown and ArrowUp move between visible rows, ArrowRight opens a closed folder or steps into an open one, ArrowLeft closes an open folder or jumps to the parent, Home and End go to the first and last row, PageUp and PageDown jump ten rows, Enter opens a file or toggles a folder, Space selects (or checks with checkboxes), Shift + arrows extend a multiple selection, Ctrl+A (Cmd+A on a Mac) selects every visible row, * opens every folder beside the focused one, typing letters jumps to the next row that starts with them (diacritics ignored), Escape clears a multiple selection, and Shift+F10 or the ContextMenu key opens the row's ⋯ menu. With the filter field, typing filters as you go: matches keep their parents, the path to them opens and matched letters turn bold; ArrowDown moves into the tree and Escape clears the field. Folders with lazy children show a spinner row while they load and a Retry row when loading fails. With draggable on, drag a row with the mouse (or long press it on touch) and a line shows where it lands: before or after a row, or a ring around a folder it goes into; hovering a closed folder opens it, and dropping a folder into itself is refused. Alt + ArrowUp and Alt + ArrowDown move the focused row among its siblings, Alt + ArrowRight moves it into the folder above and Alt + ArrowLeft moves it out one level; every move is announced. On a phone rows grow to 44px, the ⋯ button is always visible and opens a bottom sheet, the whole row is the tap target and the filter field uses 16px text. Huge trees only render the rows in view.

## Installation

```bash
npx wingo-ui@latest add tree-view
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/tree-view` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/tree-view.tsx`

npm dependencies: `npm i @dnd-kit/core @tanstack/react-virtual motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useDebounce](https://wingo-ui.com/components/use-debounce) ([Markdown](https://wingo-ui.com/components/use-debounce.md))
- [useSelection](https://wingo-ui.com/components/use-selection) ([Markdown](https://wingo-ui.com/components/use-selection.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { TreeView, moveTreeNode, type TreeNode } from "@/components/ui/tree-view"

const documents: TreeNode[] = [
  {
    id: "clients",
    label: "Clients",
    children: [
      {
        id: "tech",
        label: "Tech Solutions Inc.",
        children: [
          { id: "contract", label: "Contract 2026.pdf" },
          { id: "invoice", label: "Invoice 2026-0412.pdf" },
        ],
      },
      { id: "logistic", label: "Summit Logistics", hasChildren: true },
    ],
  },
  { id: "reports", label: "Reports", children: [{ id: "q3", label: "Q3 2026.xlsx" }] },
]

export function Documents({ onOpen }: { onOpen: (id: string) => void }) {
  const [items, setItems] = useState(documents)
  return (
    <TreeView
      items={items}
      folderIcons
      searchable
      draggable
      defaultExpandedKeys={["clients", "tech"]}
      loadChildren={async () => [{ id: "framework", label: "Framework contract 2025.pdf" }]}
      onAction={(node) => onOpen(node.id)}
      onMove={(event) => setItems((prev) => moveTreeNode(prev, event))}
      labels={{ tree: "Documents", search: "Search documents" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `TreeNode[]` |  | The tree: { id, label, textValue?, icon?, expandedIcon?, children?, hasChildren?, disabled?, badge?, description?, href?, actions?: MenuEntry[], draggable?, droppable? }. Ids must be unique across the whole tree (a dev warning names duplicates). A non-string label needs textValue for filtering and type-ahead. |
| `selectionMode?` | `"none" \| "single" \| "multiple"` | `"single"` | none: rows only open and act. single: one selected row. multiple: clicks toggle rows, Shift extends a range, Ctrl/Cmd+A selects every visible row. |
| `selectedKeys?` | `string[]` |  | Selected ids (controlled). Pair with onSelectedKeysChange. |
| `defaultSelectedKeys?` | `string[]` |  | Selected ids while uncontrolled. Default []. |
| `onSelectedKeysChange?` | `(keys: string[]) => void` |  | Fires with the new selection, in display order. |
| `checkboxes?` | `boolean` | `false` | Tri-state checkboxes before the icons, independent of selection: the box checks, the row selects. |
| `checkedKeys?` | `string[]` |  | Checked ids (controlled). With cascade, fully checked parents are part of the keys too. |
| `defaultCheckedKeys?` | `string[]` |  | Checked ids while uncontrolled. Default []. |
| `onCheckedKeysChange?` | `(keys: string[]) => void` |  | Fires with the new checked ids. |
| `cascade?` | `boolean` | `true` | Checking a parent checks its (loaded, enabled) descendants; a parent is checked when all of them are and shows a dash when some are. Disabled rows are skipped. Needs checkboxes. |
| `expandedKeys?` | `string[]` |  | Open folder ids (controlled). Ids of nodes that do not exist are ignored. |
| `defaultExpandedKeys?` | `string[]` |  | Open folder ids while uncontrolled. Default []. |
| `onExpandedKeysChange?` | `(keys: string[]) => void` |  | Fires when a folder opens or closes, including the folders a filter opens. |
| `defaultExpandAll?` | `boolean` | `false` | Starts with every folder that has children open (lazy folders stay closed). |
| `expandOnClick?` | `boolean` | `true` | Clicking a folder row also opens or closes it, like a file explorer. Off: only the chevron and the keyboard do. |
| `loadChildren?` | `(node: TreeNode) => Promise<TreeNode[]>` |  | Lazy children: a node with hasChildren and no children loads on first expand, with a spinner row; a rejected promise shows a Retry row. Results are cached inside the tree. Filtering only sees loaded nodes. |
| `onAction?` | `(node: TreeNode) => void` |  | Enter, double click or a tap on a leaf: open the file. Without it a node with href follows its link. |
| `searchable?` | `boolean` | `false` | A filter field above the tree (16px text on phones, Escape clears it, ArrowDown enters the tree). |
| `filter?` | `string` | `""` | The filter text (controlled, pair with onFilterChange); works without the field too. Case and diacritics never matter, matches keep their ancestors and open the path to them, debounced 150ms. |
| `onFilterChange?` | `(filter: string) => void` |  | Fires on every keystroke in the filter field. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Rows 28 / 32 / 36px with 13 / 14 / 15px text; 44px on touch screens. |
| `indent?` | `number` | `16` | Pixels per level. |
| `guides?` | `boolean` | `true` | Vertical indent guides; the guide of the focused row's branch is darker. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The selected row (neutral is a gray tint, other tones their soft fill and text) and the checkboxes. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone for the selected row and the checkboxes. |
| `folderIcons?` | `boolean` | `false` | Folder / open folder icons for parents and a file icon for leaves, for nodes without their own icon. |
| `draggable?` | `boolean` | `false` | Drag rows (long press on touch) to reorder them or drop them into folders, and Alt + arrows to move with the keyboard. Needs onMove. |
| `onMove?` | `(event: TreeMoveEvent) => void` |  | { id, targetId, position: "before" \| "after" \| "inside", fromParentId, toParentId, index }. index is the position among the new siblings; moveTreeNode(items, event) applies it to your data. |
| `virtualize?` | `boolean \| "auto" \| "always" \| "never"` | `"auto"` | auto renders only the rows in view above 300 visible rows; true / always and false / never force it. Virtualized trees are flat (aria-level, aria-setsize, aria-posinset), skip the expand animation and default to a 420px scroll box. |
| `maxHeight?` | `number \| string` | `0` | Makes the tree its own scroll box (px or any CSS length). 0 grows with the rows. |
| `emptyText?` | `string` | `"No items"` | Title of the empty state shown in place of the tree when items is empty. A filter without matches shows labels.noResults with a Clear filter button instead. |
| `emptyState?` | `ReactNode` |  | Replaces the built-in empty state (an EmptyState with emptyText as its title) when items is empty. |
| `labels?` | `Partial<TreeViewLabels>` |  | Strings: tree (accessible name), search, noResults, clearFilter, loading, loadError, retry, actions, cancel (the phone sheet), and the move announcements pickedUp, dropBefore, dropAfter, dropInside, notAllowed, dropped, cancelled, moved ({item}, {target}, {position}, {total}). English defaults in DEFAULT_TREE_VIEW_LABELS. |
| `classNames?` | `Partial<Record<"search" \| "viewport" \| "root" \| "item" \| "row" \| "guides" \| "chevron" \| "checkbox" \| "icon" \| "label" \| "description" \| "badge" \| "actions" \| "group" \| "loading" \| "error" \| "empty" \| "overlay", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the wrapper, merged last. |

## Parts

### flattenTree (function)

Depth-first rows with level, parentId, ancestors, index and setSize; with expandedKeys only the visible ones. Signature: (items: TreeNode[], expandedKeys?: Iterable<string>) => FlatTreeNode[].

### findNode (function)

Finds a node anywhere in the tree. Signature: (items: TreeNode[], id: string) => TreeNode | undefined.

### getAncestorIds (function)

The parent chain of a node, top level first. Handy for expandedKeys that reveal a node. Signature: (items: TreeNode[], id: string) => string[].

### getCheckedState (function)

The cascade state of a node, the same rule the checkboxes use. Signature: (node: TreeNode, checkedKeys: Iterable<string>) => boolean | "indeterminate".

### moveTreeNode (function)

Applies an onMove event to your data and returns new items; untouched branches keep their identity. Signature: (items: TreeNode[], event: TreeMoveEvent) => TreeNode[].

### DEFAULT_TREE_VIEW_LABELS (constant)

The English strings for the filter, no matches, loading and retry, the row actions menu and the drag and drop announcements. Override keys through labels.

### TreeNode (type)

One node of items: an id, a label and optional children, plus the per-node extras described under items.

### TreeMoveEvent (type)

What onMove receives after a drop: { id, targetId, position ("before", "after" or "inside"), fromParentId, toParentId, index }.

### TreeViewProps (type)

The props of TreeView, for wrappers that pass them through.

## Examples

- **Permissions with cascade**: Checking a group checks everything under it; partly checked groups show a dash. Disabled rows are skipped.
- **Searchable categories**: Matches keep their parents, open the path to them and bold the matched letters.
- **Drag to move files**: Drop before, after or into a folder; onMove plus moveTreeNode updates your data. A folder opens if you hover it.
- **5,000 rows, virtualized**: Above 300 visible rows only what is in view renders; maxHeight makes the tree its own scroll box.
- **Compact sidebar**: size="sm", no guides, a brand tone for the selected row and icons from the data.
- **Lazy children with retry**: Children load on first expand. The FTP folder fails once, then Retry loads it.

## See also

- [Accordion](https://wingo-ui.com/components/accordion) ([Markdown](https://wingo-ui.com/components/accordion.md))
- [Sidebar](https://wingo-ui.com/components/sidebar) ([Markdown](https://wingo-ui.com/components/sidebar.md))
- [List](https://wingo-ui.com/components/list) ([Markdown](https://wingo-ui.com/components/list.md))
- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))

---

Canonical: https://wingo-ui.com/components/tree-view
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
