component
An accessible, keyboard-navigable tree for hierarchical data. It implements the
WAI-ARIA tree pattern (role="tree" / treeitem, roving focus, aria-level),
supports optional checkboxes with cascading or independent selection, draws
continuous per-depth guide rails, and composes leading icons, trailing badges,
and trailing action controls. The same framework-agnostic core drives the Astro,
Svelte, and React implementations.
Pass a nested items array; nodes with children become expandable branches.
Click a branch (or press →) to expand it.
---
import { TreeView } from "@pindoba/astro-tree-view";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "src",
label: "src",
defaultExpanded: true,
children: [
{
id: "components",
label: "components",
children: [
{ id: "button", label: "Button.astro" },
{ id: "input", label: "Input.astro" },
],
},
{ id: "app", label: "app.ts" },
],
},
{
id: "tests",
label: "tests",
children: [{ id: "smoke", label: "smoke.test.ts" }],
},
{ id: "readme", label: "README.md" },
];
---
<TreeView items={items} aria-label="Project files" />The tree follows the WAI-ARIA tree keyboard model with roving focus:
Tree view supports sm, md (default), and lg — row height follows the
Control Var Contract, so a tree row matches a same-size Button or Input.
---
import { TreeView } from "@pindoba/astro-tree-view";
import { stack } from "@pindoba/styled-system/patterns";
import type { TreeNodeInput, TreeViewSize } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "root",
label: "Root",
defaultExpanded: true,
children: [
{ id: "a", label: "Child A" },
{ id: "b", label: "Child B" },
],
},
];
const sizes: TreeViewSize[] = ["sm", "md", "lg"];
---
<div class={stack({ gap: "md" })}>
{
sizes.map((size) => (
<TreeView items={items} size={size} aria-label={`Tree ${size}`} />
))
}
</div>Each node accepts a badge string (rendered through a <Badge>) and a leading
decoration via the <id>:leading slot (Astro) / leading snippet (Svelte) /
leading render prop (React) — typically a <Stamp>.
---
import { TreeView } from "@pindoba/astro-tree-view";
import Stamp from "@pindoba/astro-stamp";
import { Folder, FileText } from "@lucide/astro";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "docs",
label: "Documents",
defaultExpanded: true,
children: [
{ id: "resume", label: "resume.pdf" },
{ id: "notes", label: "notes.md" },
],
},
{ id: "todo", label: "todo.txt" },
];
---
<TreeView items={items} aria-label="Files with icons">
<Stamp slot="docs:leading" emphasis="ghost" size="sm"><Folder /></Stamp>
<Stamp slot="resume:leading" emphasis="ghost" size="sm"><FileText /></Stamp>
<Stamp slot="notes:leading" emphasis="ghost" size="sm"><FileText /></Stamp>
<Stamp slot="todo:leading" emphasis="ghost" size="sm"><FileText /></Stamp>
</TreeView>---
import { TreeView } from "@pindoba/astro-tree-view";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "inbox",
label: "Inbox",
badge: "12",
defaultExpanded: true,
children: [
{ id: "work", label: "Work", badge: "8" },
{ id: "personal", label: "Personal", badge: "4" },
],
},
{ id: "archive", label: "Archive" },
];
---
<TreeView items={items} aria-label="Mailboxes" />Enable checkboxes to render a checkbox per row. With selectionMode="multiple"
and selectionBehavior="cascade" (the default), checking a branch checks every
descendant and a partially-checked branch renders indeterminate
(aria-checked="mixed").
---
import { TreeView } from "@pindoba/astro-tree-view";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "fruit",
label: "Fruit",
defaultExpanded: true,
children: [
{ id: "apple", label: "Apple" },
{
id: "citrus",
label: "Citrus",
defaultExpanded: true,
children: [
{ id: "orange", label: "Orange" },
{ id: "lemon", label: "Lemon" },
],
},
],
},
{ id: "grain", label: "Grain" },
];
---
<TreeView
items={items}
checkboxes
selectionMode="multiple"
selectionBehavior="cascade"
defaultSelectedKeys={["orange"]}
aria-label="Select foods"
/>Use selectionBehavior="independent" for per-node checkboxes with no cascade.
---
import { TreeView } from "@pindoba/astro-tree-view";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "perms",
label: "Permissions",
defaultExpanded: true,
children: [
{ id: "read", label: "Read" },
{ id: "write", label: "Write" },
{ id: "admin", label: "Admin" },
],
},
];
---
<TreeView
items={items}
checkboxes
selectionMode="multiple"
selectionBehavior="independent"
aria-label="Permissions"
/>Place interactive controls in the row’s trailing actions slot (<id>:actions in
Astro, the actions snippet/render prop in Svelte/React). They render as
siblings of the toggle — never nested inside it — so they’re independently
focusable, and clicking one never expands or selects the row.
---
import { TreeView } from "@pindoba/astro-tree-view";
import Button from "@pindoba/astro-button";
import { Pencil, Trash2 } from "@lucide/astro";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "team",
label: "Team",
defaultExpanded: true,
children: [
{ id: "ada", label: "Ada Lovelace" },
{ id: "alan", label: "Alan Turing" },
],
},
];
---
<TreeView items={items} aria-label="Team members">
<Fragment slot="ada:actions">
<Button emphasis="ghost" size="xs" shape="square" aria-label="Edit Ada">
<Pencil />
</Button>
<Button
emphasis="ghost"
size="xs"
shape="square"
feedback="danger"
aria-label="Delete Ada"
>
<Trash2 />
</Button>
</Fragment>
<Fragment slot="alan:actions">
<Button emphasis="ghost" size="xs" shape="square" aria-label="Edit Alan">
<Pencil />
</Button>
<Button
emphasis="ghost"
size="xs"
shape="square"
feedback="danger"
aria-label="Delete Alan"
>
<Trash2 />
</Button>
</Fragment>
</TreeView>Continuous per-depth guide rails connect each branch to its children, with the
active branch’s rails highlighted. Set showGuides={false} to drop them and
indent with whitespace alone.
---
import { TreeView } from "@pindoba/astro-tree-view";
import type { TreeNodeInput } from "@pindoba/core-tree-view";
const items: TreeNodeInput[] = [
{
id: "a",
label: "Parent",
defaultExpanded: true,
children: [
{ id: "a1", label: "Child one" },
{
id: "a2",
label: "Child two",
defaultExpanded: true,
children: [{ id: "a2a", label: "Grandchild" }],
},
],
},
];
---
<TreeView items={items} showGuides={false} aria-label="No guide rails" />tree pattern: role="tree" on the container,
role="treeitem" per row, with aria-level, aria-setsize, aria-posinset,
and aria-expanded on branches.tabindex keeps a single tab stop; arrow keys move the cursor and DOM
focus together.aria-selected, or aria-checked
(including the mixed tri-state) when checkboxes is enabled.Trailing action controls (sibling of the toggle, outside it).
Leading action controls (sibling of the toggle, outside it).
Place the keyboard cursor on the first node on mount.
falseWhether to render a checkbox at the start of every row.
Initial expanded branches (uncontrolled).
Initial selection (uncontrolled).
Keys that cannot be selected or focused.
Bindable element reference for the root `<ul role="tree">`.
"default"Visual emphasis of the container surface.
Bindable expanded branches.
Bindable keyboard cursor (read-only — drive it via keyboard / pointer).
The tree's data — a nested array of nodes. A node is `{ id, label?, children?, disabled?, badge?, leading?, trailing? }`; nesting `children` builds the hierarchy.
Leading decoration per node (icon / Stamp). Receives the rendered item.
Fires when a node is activated (click / Enter).
Fires whenever the set of expanded branches changes.
Fires whenever the selection changes.
Per-slot escape hatch to inject Panda styles or HTML attributes into any slot of the component.
Extra HTML attributes spread onto the root `tree` element.
Bindable current selection / checked keys.
"cascade"How a checkbox toggle propagates: `"cascade"` (parent ⇄ descendants, with indeterminate parents) or `"independent"` (each node alone).
"single"How clicks / Enter / Space resolve into a selection. `"none"` activates without persisting selection.
trueWhether to draw the per-depth connecting guide rails.
"md"Visual size variant — controls font size, row height, and indent width.
Trailing decoration per node, before any actions.
Plus all standard <ul> HTML
attributes.