Structure
Tree
Folders that open, rows that drag, checkboxes that tell the truth about what's underneath.
- herbs
- basil.md
- coriander.md
- mint
- spearmint.md
- peppermint.md
- last summer
- watering-rota.md
Drag a note somewhere better, or press F2 to rename it. Alt+Shift+arrows move things without a mouse. “last summer” loads when asked.
- Greenhouse
- Water the seedlings
- Move trays around
- Adjust the thermostat
- Shed
- Borrow the good trowel
- Take the wheelbarrow home
Tick a heading and everything under it follows; untick one child and the heading goes half-lit.
Show codeHide code
"use client";
import { useState } from "react";
import { insertChildren, moveNode } from "@/registry/lilt/lib/tree";
import type { TreeMoveDetail, TreeNode } from "@/registry/lilt/lib/tree";
import { FileIcon, FolderIcon } from "@/registry/lilt/ui/icons";
import {
Tree,
TreeItem,
TreeItemGroup,
TreeItemLabel,
TreeView,
} from "@/registry/lilt/ui/tree";
const folder = <FolderIcon size={16} />;
const file = <FileIcon size={16} />;
const initialFiles: TreeNode[] = [
{
children: [
{ icon: file, label: "basil.md", value: "basil" },
{ icon: file, label: "coriander.md", value: "coriander" },
{
children: [
{ icon: file, label: "spearmint.md", value: "spearmint" },
{ icon: file, label: "peppermint.md", value: "peppermint" },
],
icon: folder,
label: "mint",
value: "mint",
},
],
icon: folder,
label: "herbs",
value: "herbs",
},
{
// No children yet — expanding it calls onLoadChildren.
hasChildren: true,
icon: folder,
label: "last summer",
value: "archive",
},
{ icon: file, label: "watering-rota.md", value: "rota" },
];
const permissions = [
{
children: [
{ label: "Water the seedlings", value: "water" },
{ label: "Move trays around", value: "trays" },
{ label: "Adjust the thermostat", value: "thermostat" },
],
label: "Greenhouse",
value: "greenhouse",
},
{
children: [
{ label: "Borrow the good trowel", value: "trowel" },
{ label: "Take the wheelbarrow home", value: "wheelbarrow" },
],
label: "Shed",
value: "shed",
},
];
const renameNode = (
nodes: TreeNode[],
value: string,
label: string
): TreeNode[] =>
nodes.map((node) => {
if (node.value === value) {
return { ...node, label };
}
return node.children
? { ...node, children: renameNode(node.children, value, label) }
: node;
});
const sleep = (ms: number) =>
// oxlint-disable-next-line promise/avoid-new -- the demo fakes network latency so the lazy branch actually shows its spinner
new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});
export default function TreeDemo() {
const [files, setFiles] = useState(initialFiles);
const [selected, setSelected] = useState<string[]>(["basil"]);
const [expanded, setExpanded] = useState<string[]>(["herbs"]);
const [granted, setGranted] = useState<string[]>(["water", "trays"]);
const handleMove = (detail: TreeMoveDetail) =>
setFiles((current) => moveNode(current, detail));
const handleRename = (value: string, label: string) =>
setFiles((current) => renameNode(current, value, label));
const handleLoad = async (value: string) => {
await sleep(900);
setFiles((current) =>
insertChildren(current, value, [
{ icon: file, label: "tomatoes-that-failed.md", value: "tomatoes" },
{ icon: file, label: "one-good-courgette.md", value: "courgette" },
])
);
};
return (
<div className="flex w-full max-w-md flex-col gap-10">
<div className="flex flex-col gap-2">
<TreeView
aria-label="Garden notes"
expanded={expanded}
guides
items={files}
onExpandedChange={setExpanded}
onLoadChildren={handleLoad}
onMove={handleMove}
onRename={handleRename}
onSelectedChange={setSelected}
renamable
reorderable
selected={selected}
/>
<p className="text-xs text-[var(--lilt-text-muted)]">
Drag a note somewhere better, or press F2 to rename it.
Alt+Shift+arrows move things without a mouse. “last
summer” loads when asked.
</p>
</div>
<div className="flex flex-col gap-2">
<Tree
aria-label="Volunteer permissions"
checkboxSelection
defaultExpanded={["greenhouse", "shed"]}
onSelectedChange={setGranted}
selected={granted}
>
{permissions.map((group) => (
<TreeItem key={group.value} value={group.value}>
<TreeItemLabel>{group.label}</TreeItemLabel>
<TreeItemGroup>
{group.children.map((item) => (
<TreeItem key={item.value} value={item.value}>
<TreeItemLabel>{item.label}</TreeItemLabel>
</TreeItem>
))}
</TreeItemGroup>
</TreeItem>
))}
</Tree>
<p className="text-xs text-[var(--lilt-text-muted)]">
Tick a heading and everything under it follows; untick one child and
the heading goes half-lit.
</p>
</div>
</div>
);
}Installation
npx shadcn@latest add @lilt/treeAccessibility
- One tab stop for the whole tree: arrows walk it, Right opens a branch, Left closes or climbs, and typing jumps by name.
- Single-select trees announce via aria-selected; turn on multiSelect or checkboxSelection and they switch to aria-checked, never both.
- Alt+Shift+arrows reorder without a mouse — dragging is never the only way — and every move is announced politely.
- Give the tree an aria-label; the pattern requires a named tree and nothing else supplies the name.