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>

Surface

The tree container is a Panel root, so it takes every Panel surface prop — background, feedback, padding, radius (including radius="inner" for a concentric corner inside a Card or Dialog), border, shadow, translucent, and as.

appearance is the shorthand that presets them: "default" (the raised, padded surface) or "subtle" (flush — transparent, unpadded, no shadow). Any Panel prop passed alongside it overrides just that axis.

<TreeView appearance="subtle" items={items} />
<TreeView radius="inner" padding="xs" items={items} />

emphasis="default" / emphasis="subtle" still work as a deprecated alias for appearance. Every other emphasis value is the Panel emphasis ramp ("primary" | "secondary" | "tertiary", defaulting to "secondary").

Nested inside a Card, radius="inner" keeps the tree’s corner concentric with the Card’s — it resolves as the Card’s radius minus its content padding, so the two curves stay in step whatever the Card’s radius and size are.

The tree container is a Panel root, so the default appearance surface takes radius="inner" and curves concentrically with the Card around it.

  • src
  • index.ts
  • types.ts
  • README.md

Default radius.

  • src
  • index.ts
  • types.ts
  • README.md

radius="inner".

---
import { TreeView } from "@pindoba/astro-tree-view";
import Card from "@pindoba/astro-card";
import { css } from "@pindoba/styled-system/css";
import { stack, grid } from "@pindoba/styled-system/patterns";
import type { TreeNodeInput } from "@pindoba/core-tree-view";

const items: TreeNodeInput[] = [
  {
    id: "src",
    label: "src",
    defaultExpanded: true,
    children: [
      { id: "index", label: "index.ts" },
      { id: "types", label: "types.ts" },
    ],
  },
  { id: "readme", label: "README.md" },
];
---

<div class={stack({ gap: "md", direction: "column", width: "full" })}>
  <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
    The tree container is a Panel root, so the default <code>appearance</code> surface
    takes <code>radius="inner"</code> and curves concentrically with the Card around
    it.
  </p>
  <div class={grid({ columns: 2, gap: "lg" })}>
    <Card size="md" radius="2xl" border="default">
      <TreeView items={items} aria-label="Files, default radius" />
      <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
        Default radius.
      </p>
    </Card>
    <Card size="md" radius="2xl" border="default">
      <TreeView items={items} radius="inner" aria-label="Files, inner radius" />
      <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
        <code>radius="inner"</code>.
      </p>
    </Card>
  </div>
</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 · 44 shown · 44 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).

active
boolean"current""checked""current-within"
default false

Mark this as the chosen one in a set (a current nav item, a checked option, a selected card). Where `interactive` says it *can* be clicked, `active` says it *is* the current choice. Use `activeEmphasis` to control how loud the state is. Combined with `interactive`, hover and press restart their stepping from the active baseline rather than falling back to the unchosen ramp. The three values differ only in what drives the state: `true` by the prop (via `data-panel-active`), `"checked"` by a wrapped native input (`:has(input:checked)` — zero JS, nothing to re-render), and `"current"` by an existing `aria-current`.

activeEmphasis
"primary""secondary""tertiary"
default the panel's `emphasis`

How loud the `active` state is. Defaults to the panel's own `emphasis`, so the chosen state lands on the same ramp as the resting look — this prop is the override for when the active item should be louder (or quieter) than the panel itself. `"primary"` fills with the feedback accent ramp and flips text to the contrast scale (~150 RGB units of separation — the option that reads at a glance in a dense list). `"secondary"` tints the feedback surface ramp and `"tertiary"` the neutral ramp; both promote text to `bold` and lift the border to the default line, but move the surface only ~7–18 units.

appearance
"default""subtle"
default "default"

The container surface preset: `"default"` is the raised, padded card-like surface; `"subtle"` is flush (transparent, no padding, no shadow). It only sets Panel defaults — pass any Panel prop (`background`, `padding`, `radius`, `shadow`, `border`, …) alongside it to override that axis.

as
"div""section""article""aside""main""header""footer""nav""dialog""form""fieldset""ul""ol""li""a""button""label""span""kbd"
default "ul"

HTML element to render the tree container as.

autoFocus svelte
boolean

Place the keyboard cursor on the first node on mount.

background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).

border
"none""bold""default""muted""accent"

Border style: `none`, `default`, `bold`, or `muted`.

borderActive
"none""bold""default""inherit""muted""accent"

Border color while the surface is `active` — the explicit form of the ring `activeEmphasis` would otherwise pick. It holds through hover, so a `borderInteract` color can't steal it. `inherit` keeps whatever `activeEmphasis` chose; `none` clears it.

borderInteract
"none""bold""default""muted""accent"

Border style applied on hover / focus interaction.

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
HTMLElementnull

Bindable element reference for the root `role="tree"` container (a `<ul>` unless `as` says otherwise).

emphasis
"primary""default""subtle""secondary""tertiary"
default "secondary"

The Panel emphasis ramp of the container surface. Passing `"default"` or `"subtle"` is a **deprecated** alias for {@link TreeViewBaseProps.appearance} and is mapped to it.

expandedKeys svelte
Set<Key>

Bindable expanded branches.

feedback
"primary""neutral""success""warning""danger""inherit"

Semantic color tone: `neutral`, `primary`, `success`, `warning`, `danger`, or `inherit`.

focusedKey svelte
Keynull

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

interactive
boolean

Enable interactive (hover / focus / press) affordances and states.

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.

padding
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""7xl""8xl""4xs""3xs""2xs""9xl""10xl""11xl"

Inner padding from the spacing scale.

passThrough
TreeViewPassThrough

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

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override from the spacing scale (or `full` for fully rounded).

radiusBottom
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the bottom-left and bottom-right corners.

radiusLeft
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-left and bottom-left corners.

radiusRight
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-right and bottom-right corners.

radiusTop
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-left and top-right corners.

reactive
boolean
default false

Serialize the panel's resolved surface inputs into a `data-panel-config` attribute on the root, enabling runtime surface changes via `updatePanelElement()` (and, for inheritors like Card, `updateCard()`) without a framework runtime — the primary consumer is Astro / vanilla JS, where props don't re-render. Reactive frameworks (Svelte/React/Vue) don't need this: their prop changes re-run the connect already.

reserveBorderSpace
boolean

Keep (or drop) the always-1px transparent border the panel reserves so a border appearing or changing never shifts layout. Computed automatically — it's reserved when a resting `border` is visible, or when `interactive` / `borderInteract` / `active` can change the border at runtime. Set `false` only on a panel that must not occupy that 1px (e.g. a borderless housing frame that would otherwise add 2px around a set).

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.

shadow
"sm""md""lg""xl""none""xs"

Elevation shadow applied to the surface.

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.

translucent
boolean

Apply a frosted-glass effect with backdrop blur over a surface background.

Plus all standard <ul> HTML attributes.

Type

  • Components
  • Blocks