4 Tree
L4stIdi0t edited this page 2026-10-09 22:01:06 +02:00

Tree

Accessible tree view (APG tree pattern): role=tree/treeitem/group, roving tabindex (one item tabbable), aria-expanded/selected.

Keyboard: ↑/↓ move through visible items, → expand / enter folder, ← collapse / go to parent, Home/End, Enter/Space activate. Children can be loaded on demand, so a filesystem does not have to be walked up front. Names are shown whole; a tree wider than its box scrolls left and right instead.

Navigation · two-way: bind:selectedId · keyboard contract

<script lang="ts">
	import { Tree } from '@l4st/driftwood';
</script>
Light Dark
tree tree in dark mode

Props

Prop Type Default What it does
items required TreeNode[] — The whole tree. A node with children renders as a folder.
selectedId bind: string — Id of the selected node.
defaultExpanded string[] [] Ids expanded on first render.
class string — Merged with the component's own classes, last — so yours win.
aria-label string 'File tree' Names the tree, e.g. "Files".
onSelect (node: TreeNode) => void — Fires with the node that was selected.
loadChildren (node: TreeNode) => TreeNode[] | Promise<TreeNode[]> — Fetches a folder's children the first time it opens. Give the node hasChildren: true and no children, and this is awaited on expand; the result is kept, so it runs once per node.
onLoadError (node: TreeNode, error: unknown) => void — Fires when loadChildren throws; the node collapses and offers a retry.
loadingLabel string 'Loading' Announced while a folder's children are loading.
retryLabel string 'Could not load — activate to try again' Title of a folder whose load failed; activating it tries again.

Types

export type TreeNode = {
	id: string;
	label: string;
	children?: TreeNode[];
	/** Renders as a folder before its children exist — the lazy-loading flag. */
	hasChildren?: boolean;
};

Keyboard

Key Does
↑ / ↓ move between visible nodes
→ expand a node, or move into it
← collapse a node, or move to its parent
Home / End first / last visible node
Enter / Space select the focused node

Example

The markup behind the screenshot above, lifted from src/routes/shots/ — so it compiles, and it is exactly what produced the image.

<Tree items={tree} selectedId="media" defaultExpanded={['volume1']} />

Notes

Loading a subtree on demand

A whole filesystem is rarely worth walking up front. Mark a folder with hasChildren and leave children off, and loadChildren is called the first time it opens — by a click, by →, or by defaultExpanded naming it:

<Tree
	items={[{ id: '/', label: 'volume1', hasChildren: true }]}
	loadChildren={async (node) => {
		const res = await fetch(`/api/files?path=${encodeURIComponent(node.id)}`);
		return await res.json(); // TreeNode[] — each folder marked hasChildren
	}}
	onLoadError={(node, err) => toast.error(`Could not open ${node.label}`)}
/>
  • The result is kept per node, so collapsing and reopening does not fetch again. The tree owns that cache; items stays the roots you passed.
  • While a folder loads, its row is aria-busy and a spinner replaces the chevron. loadingLabel names it for screen readers.
  • If loadChildren throws, the node collapses back, shows a struck folder with retryLabel on it, and calls onLoadError. Activating it tries again — so the retry is the same gesture as the open, and there is nothing to reset.
  • A folder that loads to [] stays a folder; it just opens onto nothing.
  • Mixing is fine: nodes that already have children never call the callback.

Navigation · Tabs · Pagination · CommandPalette · Kbd


← All components · Getting started · Theming · source