Skip to content

Tree

import { Tree } from '@delightstack/components';
import type { TreeNode, FlatTreeNode } from '@delightstack/components';
  • src
    • components
      • Button.svelte
      • Modal.svelte
      • Table.svelte
    • app.ts
    • index.ts
  • 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 />
  • src
    • components
      • Button.svelte
      • Modal.svelte
    • app.ts
  • package.json
  • README.md

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
/>

Parent checkboxes reflect children state with an indeterminate state when partially selected. Checking a parent checks all children.

  • Fruits
    • Apple
    • Banana
    • Cherry
  • Vegetables
    • Carrot
    • Broccoli
    • Spinach
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 />

The component automatically detects flat data (arrays with parentId properties) and builds the tree structure internally.

  • Root
    • Child 1
      • Grandchild 1
      • Grandchild 2
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
/>

Nodes matching the filter term are highlighted. Non-matching branches without matching descendants are hidden, and parent nodes auto-expand to reveal matches.

  • 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} />

Enable drag-and-drop reordering between nodes.

  • Documents
    • report.pdf
    • notes.txt
  • Images
    • photo1.png
    • photo2.jpg
  • README.md
View code
<Tree
data={fileTree}
draggable
ondrop={({ node, target, position }) => {
updateTree(node, target, position);
}}
/>

Load children on demand when a node is expanded. A loading spinner replaces the chevron during the fetch.

View code
<Tree
data={rootNodes}
load_children={async (node) => {
return await fetchChildren(node.id);
}}
/>
  • src
    • lib
      • auth.ts
      • db.ts
    • routes
      • +page.svelte
      • about/+page.svelte
    • app.html
  • static
    • favicon.png
  • package.json
View code
<Tree data={fileTree} show_lines bind:expanded />
  • src 4 files
    • index.ts entry
    • app.ts
    • config.ts modified
    • utils.ts
  • 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} />

Use dense for compact node spacing or comfortable for relaxed spacing. The default sits between the two.

Dense
  • src
    • components
      • Button.svelte
      • Modal.svelte
    • app.ts
    • index.ts
  • package.json
  • README.md
Comfortable
  • src
    • components
      • Button.svelte
      • Modal.svelte
    • app.ts
    • index.ts
  • package.json
  • README.md
View code
<Tree data={fileTree} dense show_lines />
<Tree data={fileTree} comfortable show_lines />
View code
<Tree skeleton={loading} skeleton_count={7} skeleton_depth={3} data={loading ? [] : data} />
PropTypeDefaultDescription
dataTreeNode[] | FlatTreeNode[]requiredTree data (nested or flat with parent IDs)
selectedstring[][]Selected node IDs (bindable)
expandedstring[][]Expanded node IDs (bindable)
selectablebooleanfalseEnable node selection
multi_selectbooleanfalseAllow multiple selection
checkboxesbooleanfalseShow checkboxes for selection
show_linesbooleanfalseShow connecting lines between nodes
draggablebooleanfalseEnable drag-and-drop reordering
filterstring-Search/filter term to highlight and filter matches
densebooleanfalseCompact node spacing
comfortablebooleanfalseRelaxed node spacing
skeletonbooleanfalseShow loading skeleton
skeleton_countnumber5Number of skeleton nodes
skeleton_depthnumber2Nesting depth of skeleton nodes
idstringautoElement ID
classstring''Additional CSS classes
load_children(node: TreeNode) => Promise<TreeNode[]>-Lazy load children on expand
node_contentSnippet<[{ node: TreeNode; level: number }]>-Custom node content renderer
EventDetailDescription
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

The nested tree data format:

interface TreeNode {
id: string;
label: string;
icon?: Component;
children?: TreeNode[];
disabled?: boolean;
data?: unknown;
}

The flat data format with parent references:

interface FlatTreeNode {
id: string;
parentId: string | null;
label: string;
icon?: Component;
disabled?: boolean;
data?: unknown;
}
KeyAction
Arrow DownMove focus to next visible node
Arrow UpMove focus to previous visible node
Arrow RightExpand focused node, or move to first child if already expanded
Arrow LeftCollapse focused node, or move to parent if already collapsed
Enter / SpaceSelect or toggle checkbox on focused node
HomeMove focus to first visible node
EndMove focus to last visible node
* (asterisk)Expand all siblings of the focused node
  • Container has role="tree", each node has role="treeitem"
  • aria-expanded on expandable nodes
  • aria-selected on selectable nodes
  • aria-level indicating nesting depth
  • aria-activedescendant for focus management
  • aria-multiselectable when multiple selection is enabled
  • aria-disabled on 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