X Data

Star on GitHubFeedback
X DataNative HTMLot-tree

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 Tree

Example

  • Harbour Line, 4 items
    • 12 kB
    • Type, 3 items
      • 184 kB
      • 176 kB
      • 181 kB
    • Posters, 2 items
      • 8.2 MB
      • 6.9 MB
    • 40 kB
  • Salt & Rye, 2 items
    • 96 kB
    • 2.4 MB
  • Drafts, 0 items

    Empty

  • 8 kB
  • Silence, 2 items1
    • 3
    • 11
  • Measure, 3 items19
    • 21
    • Rhythm, 2 items30
      • 32
      • 37
    • 44
  • 51
All closed
  • Harbour Line, 4 items
    • 12 kB
    • Type, 3 items
      • 184 kB
      • 176 kB
      • 181 kB
    • Posters, 2 items
      • 8.2 MB
      • 6.9 MB
    • 40 kB
  • Salt & Rye, 2 items
    • 96 kB
    • 2.4 MB
  • Drafts, 0 items

    Empty

  • 8 kB
A long folder ends in the rest
  • Kestrel Press, 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
  • Drafts, 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.json

Usage

tree-example.tsx
"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; item rows for leaves (a <button>, or a link with href); collapsible for a long folder's rest.
  • Anatomy: <div class="ot-tree" data-variant="outline"> of ul.ot-tree-list; each li.ot-tree-node (data-kind="leaf | folder") is a leaf .ot-item.ot-tree-row holding .ot-tree-name, or a details.ot-tree-folder whose summary.ot-tree-row holds the name, its count as a raised sup.ot-tree-count (read as ", 3 items") and an optional value after the item's leader. A folder longer than limit ends in li.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

Props of Tree
PropTypeDefaultWhat it does
nodesTreeNode[]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 / onSelectedChangestring | nullThe chosen leaf, controlled or not. Its dot (or number) takes the accent and its name turns italic; it carries aria-current.
expanded / defaultExpanded / onExpandedChangestring[]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.
limitnumbernoneA 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

Where its move comes from
ComponentReferenceMove
TreeContents pages; Paul Rand's dimension linesNames on hairline branches, each hanging from its folder's first letter; ring, dot and accent
Outline treeThe book's decimal contents; "the silence that heals" numbers as dataThe depth is in the numbers: 1, 1.2, 1.2.3

How it moves

How it moves
ComponentArticulationWhat moves
TreeGrowOpening a folder draws its branch down and its names arrive in turn; the chosen dot lands