component

Tree View

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.

Default

Pass a nested items array; nodes with children become expandable branches. Click a branch (or press ) to expand it.

  • src
  • app.ts
  • README.md
---
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" />

Keyboard

The tree follows the WAI-ARIA tree keyboard model with roving focus:

  • / — move to the previous / next visible node.
  • — expand a collapsed branch, or step into its first child.
  • — collapse an expanded branch, or step to the parent.
  • Home / End — jump to the first / last visible node.
  • Enter activates (selects); Space toggles the checkbox / selection. Type-ahead jumps to the next node whose label matches.

Sizes

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.

  • Root
  • Child A
  • Child B
  • Root
  • Child A
  • Child B
  • Root
  • Child A
  • Child B
---
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>

Icons and badges

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>.

  • Documents
  • resume.pdf
  • notes.md
  • todo.txt
---
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>
  • Inbox12
  • Work8
  • Personal4
  • Archive
---
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" />

Checkboxes (cascading)

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").

  • Fruit
  • Apple
  • Citrus
  • Orange
  • Lemon
  • Grain
---
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"
/>

Checkboxes (independent)

Use selectionBehavior="independent" for per-node checkboxes with no cascade.

  • Permissions
  • Read
  • Write
  • Admin
---
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"
/>

Trailing actions

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.

  • Team
  • Ada Lovelace
  • Alan Turing
---
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>

Guide rails

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.

  • Parent
  • Child one
  • Child two
  • Grandchild
---
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" />

Accessibility

  • Implements the WAI-ARIA tree pattern: role="tree" on the container, role="treeitem" per row, with aria-level, aria-setsize, aria-posinset, and aria-expanded on branches.
  • Roving tabindex keeps a single tab stop; arrow keys move the cursor and DOM focus together.
  • Selection state is announced through aria-selected, or aria-checked (including the mixed tri-state) when checkboxes is enabled.
  • Trailing/leading action controls are siblings of the toggle, never nested inside an interactive element.
props · 24 shown · 24 total
actions slot svelte
Snippet<[TreeItemApi]>

Trailing action controls (sibling of the toggle, outside it).

actionsLeading slot svelte
Snippet<[TreeItemApi]>

Leading action controls (sibling of the toggle, outside it).

autoFocus svelte
boolean

Place the keyboard cursor on the first node on mount.

checkboxes
boolean
default false

Whether to render a checkbox at the start of every row.

defaultExpandedKeys svelte
Iterable<Key>

Initial expanded branches (uncontrolled).

defaultSelectedKeys svelte
Iterable<Key>

Initial selection (uncontrolled).

disabledKeys svelte
Iterable<Key>

Keys that cannot be selected or focused.

element binding svelte
HTMLUListElementnull

Bindable element reference for the root `<ul role="tree">`.

emphasis
"default""subtle"
default "default"

Visual emphasis of the container surface.

expandedKeys svelte
Set<Key>

Bindable expanded branches.

focusedKey svelte
Keynull

Bindable keyboard cursor (read-only — drive it via keyboard / pointer).

items required
TreeNodeInput[]

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 slot svelte
Snippet<[TreeItemApi]>

Leading decoration per node (icon / Stamp). Receives the rendered item.

onAction svelte
(key: Key) => void

Fires when a node is activated (click / Enter).

onExpandedChange svelte
(keys: Set<Key>) => void

Fires whenever the set of expanded branches changes.

onSelectionChange svelte
(keys: Set<Key>) => void

Fires whenever the selection changes.

passThrough
TreeViewPassThrough

Per-slot escape hatch to inject Panda styles or HTML attributes into any slot of the component.

rootAttrs
RootAttributes

Extra HTML attributes spread onto the root `tree` element.

selectedKeys svelte
Set<Key>

Bindable current selection / checked keys.

selectionBehavior
"cascade""independent"
default "cascade"

How a checkbox toggle propagates: `"cascade"` (parent ⇄ descendants, with indeterminate parents) or `"independent"` (each node alone).

selectionMode
"none""single""multiple"
default "single"

How clicks / Enter / Space resolve into a selection. `"none"` activates without persisting selection.

showGuides
boolean
default true

Whether to draw the per-depth connecting guide rails.

size
"sm""md""lg"
default "md"

Visual size variant — controls font size, row height, and indent width.

trailing slot svelte
Snippet<[TreeItemApi]>

Trailing decoration per node, before any actions.

Plus all standard <ul> HTML attributes.

Type

  • Components
  • Blocks