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 |
|---|---|
![]() |
![]() |
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;
itemsstays the roots you passed. - While a folder loads, its row is
aria-busyand a spinner replaces the chevron.loadingLabelnames it for screen readers. - If
loadChildrenthrows, the node collapses back, shows a struck folder withretryLabelon it, and callsonLoadError. 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
childrennever call the callback.
Navigation · Tabs · Pagination · CommandPalette · Kbd
driftwood
Layout
Actions
Forms
- Field
- Input
- FileInput
- FileList
- Textarea
- CodeEditor
- Combobox
- RadioGroup
- NumberInput
- InlineEdit
- Label
- Select
- MultiSelect
- DatePicker
- TagsInput
- Checkbox
- Switch
- Slider
Feedback
Charts
Overlays
Navigation
Generated from the source by pnpm wiki · repository · v0.24.0

