Tree
A hierarchy set as a contents page: names on hairline branches, each hanging from under its folder's first letter, a folder opened at a time.
Report an issue with TreeExample
Harbour Line4, 4 items
- 12 kB
Type3, 3 items
- 184 kB
- 176 kB
- 181 kB
Posters2, 2 items
- 8.2 MB
- 6.9 MB
- 40 kB
Salt & Rye2, 2 items
- 96 kB
- 2.4 MB
Drafts0, 0 items
Empty
- 8 kB
Silence2, 2 items1
- 3
- 11
Measure3, 3 items19
- 21
Rhythm2, 2 items30
- 32
- 37
- 44
- 51
All closed
Harbour Line4, 4 items
- 12 kB
Type3, 3 items
- 184 kB
- 176 kB
- 181 kB
Posters2, 2 items
- 8.2 MB
- 6.9 MB
- 40 kB
Salt & Rye2, 2 items
- 96 kB
- 2.4 MB
Drafts0, 0 items
Empty
- 8 kB
A long folder ends in the rest
Kestrel Press9, 9 items
- 10 kB
- 47 kB
- 84 kB
- 31 kB
and 5 moreHide these 5
- 68 kB
- 15 kB
- 52 kB
- 89 kB
- 36 kB
An empty folder, open
Drafts0, 0 items
Empty
- 8 kB
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, collapsible, and item along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/tree.jsonUsage
"use client"import { Tree, type TreeNode } from "@/components/ui/tree"// A fictional studio's archive.const archive: TreeNode[] = [{id: "harbour",label: "Harbour Line",children: [{ id: "harbour-brief", label: "Brief", value: "12 kB" },{id: "harbour-type",label: "Type",children: [{ id: "harbour-display", label: "Display, 200", value: "184 kB" },{ id: "harbour-text", label: "Text, 400", value: "176 kB" },{ id: "harbour-italic", label: "Text italic, 400", value: "181 kB" },],},{ id: "harbour-posters", label: "Posters", children: [{ id: "harbour-a1", label: "Timetable, A1", value: "8.2 MB" }, { id: "harbour-a2", label: "Night service, A2", value: "6.9 MB" }] },{ id: "harbour-invoice", label: "Invoice", value: "40 kB" },],},{ id: "salt", label: "Salt & Rye", children: [{ id: "salt-mark", label: "The mark", value: "96 kB" }, { id: "salt-menu", label: "Menu", value: "2.4 MB" }] },{ id: "drafts", label: "Drafts", children: [] },{ id: "notes", label: "Studio notes", value: "8 kB" },]// A fictional book's contents, the pages as values.const contents: TreeNode[] = [{ id: "c1", label: "Silence", value: 1, children: [{ id: "c1-1", label: "The page at rest", value: 3 }, { id: "c1-2", label: "Margins", value: 11 }] },{id: "c2",label: "Measure",value: 19,children: [{ id: "c2-1", label: "Scale", value: 21 },{ id: "c2-2", label: "Rhythm", value: 30, children: [{ id: "c2-2-1", label: "Tempo", value: 32 }, { id: "c2-2-2", label: "Rests", value: 37 }] },{ id: "c2-3", label: "Figures", value: 44 },],},{ id: "c3", label: "Voice", value: 51 },]const files = (n: number): TreeNode[] =>["Contact sheet", "Proof, first", "Proof, second", "Proof, final", "Letter", "Estimate", "Photographs", "Cover, flat", "Cover, wrap"].slice(0, n).map((label, i) => ({ id: `f${i}`, label, value: `${(i * 37) % 90 + 10} kB` }))export default function Example() {return (<div className="grid w-full max-w-xl gap-16"><Tree nodes={archive} defaultExpanded={["harbour", "harbour-type"]} defaultSelected="harbour-italic" aria-label="The studio's archive" /><Tree nodes={contents} variant="outline" defaultExpanded={["c1", "c2", "c2-2"]} defaultSelected="c2-2-2" aria-label="Contents" /></div>)}Contract
- Underneath: native
<details>and<summary>for folders;itemrows for leaves (a<button>, or a link withhref);collapsiblefor a long folder's rest. - Anatomy:
<div class="ot-tree" data-variant="outline">oful.ot-tree-list; eachli.ot-tree-node(data-kind="leaf | folder") is a leaf.ot-item.ot-tree-rowholding.ot-tree-name, or adetails.ot-tree-folderwhosesummary.ot-tree-rowholds the name, its count as a raisedsup.ot-tree-count(read as ", 3 items") and an optional value after the item's leader. A folder longer thanlimitends inli.ot-tree-tail. - branch (the default): contents pages and Paul Rand's dimension lines. A hairline branch hangs from under each open folder's first letter and every name sits on it at a dot; the last one stops the branch at its dot. A leaf is a small graphite dot, a closed folder a hairline ring, an open one a filled dot; the chosen leaf's dot is the one accent (it lands with spiccato) and its name turns italic, as yours. No icons, no boxes: the depth is read off the lines.
- outline: a book's decimal contents and the numbers-as-data of "the silence that heals": each name numbered by its place (1, 1.2, 1.2.3) in a pencil column, flush at every depth and without lines; top-level names set firm. An open folder's number inks, the chosen leaf's is the accent.
- Opening a folder you touched draws its branch down (
ot-draw-y) and its names arrive in turn; folders open from the start are simply there. The contents slide open (::details-content). - Keyboard: Tab and Enter or Space work natively. The arrows walk it as a tree: Up and Down through what shows, Right to open a folder or step into it, Left to close it or step out to its folder, Home and End to the ends; Left and Right follow the page right to left.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
nodes | TreeNode[] | The hierarchy. Each node has an id and a label; a node with children is a folder (an empty array is an empty folder), one without is a leaf. value is set at the end of its line after a dotted leader, as a contents page sets its folios. A leaf with an href is a link. | |
variant | "branch" | "outline" | "branch" | branch: a hairline spine hangs from under each open folder's first letter, and every name sits on it at a dot; a closed folder is a ring, an open one a filled dot, the chosen leaf the accent. outline: a book's decimal contents, each name numbered by its place (1, 1.2, 1.2.3) in a pencil column, flush at every depth, with no lines. |
selected / defaultSelected / onSelectedChange | string | null | The chosen leaf, controlled or not. Its dot (or number) takes the accent and its name turns italic; it carries aria-current. | |
expanded / defaultExpanded / onExpandedChange | string[] | The open folders' ids, controlled or not. A folder you open draws its branch down and its names arrive in turn; one open from the start is simply there. | |
limit | number | none | A folder longer than this shows its first rows, then "and 6 more", which opens the rest where they are, as a collapsible does. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Tree | Contents pages; Paul Rand's dimension lines | Names on hairline branches, each hanging from its folder's first letter; ring, dot and accent |
| Outline tree | The book's decimal contents; "the silence that heals" numbers as data | The depth is in the numbers: 1, 1.2, 1.2.3 |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Tree | Grow | Opening a folder draws its branch down and its names arrive in turn; the chosen dot lands |