Tree
Import
Section titled “Import”import { Tree } from '@delightstack/components';import type { TreeNode, FlatTreeNode } from '@delightstack/components';Basic Usage
Section titled “Basic Usage”- package.json
- tsconfig.json
- README.md
View code
<script> import { Tree } from '@delightstack/components';
const data = [ { id: 'src', label: 'src', children: [ { id: 'components', label: 'components', children: [ { id: 'button', label: 'Button.svelte' }, { id: 'modal', label: 'Modal.svelte' }, { id: 'table', label: 'Table.svelte' }, ], }, { id: 'utils', label: 'utils', children: [ { id: 'format', label: 'format.ts' }, { id: 'validate', label: 'validate.ts' }, ], }, { id: 'app', label: 'app.ts' }, { id: 'index', label: 'index.ts' }, ], }, { id: 'package', label: 'package.json' }, { id: 'tsconfig', label: 'tsconfig.json' }, { id: 'readme', label: 'README.md' }, ];
let expanded = $state(['src', 'components']);</script>
<Tree {data} bind:expanded show_lines />Examples
Section titled “Examples”Selectable with Icons
Section titled “Selectable with Icons”Click a file to select it. Folders expand/collapse on click.
View code
<script> import { Tree } from '@delightstack/components';
const fileTree = [ { id: '1', label: 'src', children: [ { id: '1-1', label: 'components', children: [ { id: '1-1-1', label: 'Button.svelte' }, { id: '1-1-2', label: 'Modal.svelte' }, ], }, { id: '1-2', label: 'app.ts' }, ], }, { id: '2', label: 'package.json' }, ];
let selected = $state([]); let expanded = $state(['1']);</script>
<Tree data={fileTree} selectable bind:selected bind:expanded/>Checkbox Selection
Section titled “Checkbox Selection”Parent checkboxes reflect children state with an indeterminate state when partially selected. Checking a parent checks all children.
View code
<script> import { Tree } from '@delightstack/components';
const categories = [ { id: 'fruits', label: 'Fruits', children: [ { id: 'apple', label: 'Apple' }, { id: 'banana', label: 'Banana' }, { id: 'cherry', label: 'Cherry' }, ], }, { id: 'vegetables', label: 'Vegetables', children: [ { id: 'carrot', label: 'Carrot' }, { id: 'broccoli', label: 'Broccoli' }, { id: 'spinach', label: 'Spinach' }, ], }, ];
let selected = $state([]); let expanded = $state(['fruits', 'vegetables']);</script>
<Tree data={categories} checkboxes multi_select bind:selected bind:expanded />Flat Data with Parent IDs
Section titled “Flat Data with Parent IDs”The component automatically detects flat data (arrays with parentId properties) and builds the tree structure internally.
View code
<Tree data={[ { id: '1', parentId: null, label: 'Root' }, { id: '2', parentId: '1', label: 'Child 1' }, { id: '3', parentId: '1', label: 'Child 2' }, { id: '4', parentId: '2', label: 'Grandchild 1' }, { id: '5', parentId: '2', label: 'Grandchild 2' }, { id: '6', parentId: '3', label: 'Grandchild 3' }, ]} selectable/>Search/Filter
Section titled “Search/Filter”Nodes matching the filter term are highlighted. Non-matching branches without matching descendants are hidden, and parent nodes auto-expand to reveal matches.
- src
- package.json
- README.md
View code
<script> import { Tree } from '@delightstack/components';
let searchTerm = $state('');</script>
<input bind:value={searchTerm} placeholder="Search..." /><Tree data={fileTree} filter={searchTerm} />Drag-and-Drop
Section titled “Drag-and-Drop”Enable drag-and-drop reordering between nodes.
- README.md
View code
<Tree data={fileTree} draggable ondrop={({ node, target, position }) => { updateTree(node, target, position); }}/>Lazy Loading
Section titled “Lazy Loading”Load children on demand when a node is expanded. A loading spinner replaces the chevron during the fetch.
- Team Alpha
- Team Beta
- Team Gamma
View code
<Tree data={rootNodes} load_children={async (node) => { return await fetchChildren(node.id); }}/>Connecting Lines
Section titled “Connecting Lines”- package.json
View code
<Tree data={fileTree} show_lines bind:expanded />Custom Node Content
Section titled “Custom Node Content”- package.json v2.0.0
- README.md
View code
{#snippet customNode({ node, level })} <span>{node.label}</span> {#if node.data?.badge} <span class="badge">{node.data.badge}</span> {/if}{/snippet}
<Tree data={fileTree} node_content={customNode} />Density
Section titled “Density”Use dense for compact node spacing or comfortable for relaxed spacing. The default sits between the two.
- package.json
- README.md
- package.json
- README.md
View code
<Tree data={fileTree} dense show_lines />
<Tree data={fileTree} comfortable show_lines />Skeleton Loading
Section titled “Skeleton Loading”View code
<Tree skeleton={loading} skeleton_count={7} skeleton_depth={3} data={loading ? [] : data} />| Prop | Type | Default | Description |
|---|---|---|---|
data | TreeNode[] | FlatTreeNode[] | required | Tree data (nested or flat with parent IDs) |
selected | string[] | [] | Selected node IDs (bindable) |
expanded | string[] | [] | Expanded node IDs (bindable) |
focused | string | null | null | The node the keyboard cursor is on (bindable); null when the tree is not focused |
selectable | boolean | false | Enable node selection |
multi_select | boolean | false | Allow multiple selection |
checkboxes | boolean | false | Show checkboxes for selection |
show_lines | boolean | false | Show connecting lines between nodes |
draggable | boolean | false | Enable drag-and-drop reordering |
filter | string | - | Search/filter term to highlight and filter matches |
dense | boolean | false | Compact node spacing |
comfortable | boolean | false | Relaxed node spacing |
skeleton | boolean | false | Show loading skeleton |
skeleton_count | number | 5 | Number of skeleton nodes |
skeleton_depth | number | 2 | Nesting depth of skeleton nodes |
id | string | auto | Element ID |
class | string | '' | Additional CSS classes |
load_children | (node: TreeNode) => Promise<TreeNode[]> | - | Lazy load children on expand |
node_content | Snippet<[{ node: TreeNode; level: number }]> | - | Custom node content renderer |
Events
Section titled “Events”| Event | Detail | Description |
|---|---|---|
onselect | { node: TreeNode, selected: string[] } | Selection changed |
onexpand | { node: TreeNode, expanded: boolean } | Node expanded or collapsed |
ondrop | { node: TreeNode, target: TreeNode, position: 'before' | 'after' | 'inside' } | Node dropped after drag |
TreeNode
Section titled “TreeNode”The nested tree data format:
interface TreeNode { id: string; label: string; icon?: Component; children?: TreeNode[]; disabled?: boolean; data?: unknown;}FlatTreeNode
Section titled “FlatTreeNode”The flat data format with parent references:
interface FlatTreeNode { id: string; parentId: string | null; label: string; icon?: Component; disabled?: boolean; data?: unknown;}Large Trees
Section titled “Large Trees”A collapsed branch is not rendered. The tree puts a branch’s children in the document when it opens and takes them out again once the closing animation has finished, so the number of elements on the page follows what is open rather than how much data was passed in — a 10,000-node tree with everything collapsed is twenty rows of DOM, not ten thousand.
That is invisible from the outside: expanded, selected and focused still
name any node in data, keyboard navigation still walks the whole visible order,
and filter still searches every node whether or not its branch is open. What it
does mean is that document.querySelector only finds rows inside open branches —
code reaching into the tree’s DOM should read the node data instead.
Keyboard Navigation
Section titled “Keyboard Navigation”| Key | Action |
|---|---|
| Arrow Down | Move focus to next visible node |
| Arrow Up | Move focus to previous visible node |
| Arrow Right | Expand focused node, or move to first child if already expanded |
| Arrow Left | Collapse focused node, or move to parent if already collapsed |
| Enter / Space | Select or toggle checkbox on focused node |
| Home | Move focus to first visible node |
| End | Move focus to last visible node |
| * (asterisk) | Expand all siblings of the focused node |
| Shift + Arrow Up/Down | Extend the selection one row, taking the cursor with it (multi_select) |
| Mod + A | Select every visible node (multi_select) |
| Escape | Clear the selection — claimed only when there is one to clear (multi_select) |
| Printable characters | Type-ahead — jumps to the next node whose label starts with what you typed. The buffer resets after 700ms; repeating one character cycles through the nodes starting with it |
With multi_select, clicking follows the same rules as the pointer half: mod-click toggles one
node, shift-click takes the range from the anchor, and mod+shift-click keeps the rest. The
anchor stays where the last plain or mod-click put it, so a second, closer shift-click narrows
the range rather than ratcheting it outward. The rules live in the exported selection model
(applySelection, extendSelection, selectionGestureOf), which List and Table can adopt
through the same functions.
Keystrokes that originate inside an <input>, <textarea>, <select> or a
contenteditable element — an inline rename rendered by node_content, say —
are left alone, so the control keeps its own arrows, Home/End and Enter.
Accessibility
Section titled “Accessibility”- Container has
role="tree", each node hasrole="treeitem" aria-expandedon expandable nodesaria-selectedon selectable nodesaria-levelindicating nesting deptharia-activedescendantfor focus managementaria-multiselectablewhen multiple selection is enabledaria-disabledon disabled nodes- Full keyboard navigation with arrow keys, Home, End, Enter, Space
- Expand/collapse animations respect
prefers-reduced-motion - Search matches are highlighted with
<mark>elements