Skip to main content

Tree

Info:Navigation
44

A hierarchical, keyboard-navigable list of expandable nodes built from a data array.

tree view file tree nested list hierarchy explorer outline

Live demo

live · @xtyle/astro

Tree

Documentation

Files · compact

Binder · row badges & actions

  • Chapter 1
    • Opening
    • Rising action
  • Chapter 2
  • Notes

Tree renders a nested hierarchy from an items array. Each node carries a label, an optional value, href, and children, plus flags for expanded, selected, and disabled.

It builds the WAI-ARIA tree pattern: a role="tree" with nested role="group" levels and role="treeitem" nodes carrying aria-level, aria-expanded, and aria-selected, with a single roving tab stop so the whole tree is one Tab stop and the arrow keys walk it. A twisty rotates on expand. A node with an href renders its row as a link whether or not it has children, so a branch can be both navigable and a group; the row navigates while the twisty (and Left/Right) work the children. A branch without an href is a pure container that toggles on click. A node also carries per-row trailing content: one or more badge pills (a count plus a toned status pill after the label) and actions (hover-revealed row buttons that fire a tree-action event, each optionally disabled in place), the file-tree-with-inline-controls shape. Being data-driven keeps it robust across the bindings and a natural fit for a file or navigation tree. Three sizes (sm, md, lg) scale the row density.

When to use

How this component composes with the rest of the set.

Feed it a value per node and listen for the select event to drive a detail pane or route.
Give leaf nodes an href to render them as links: the natural shape for a docs or file-navigation sidebar.
Hang a badge (a count, a status) and actions (a hover cluster of row controls) on a node for the file-tree-with-inline-controls shape; listen for tree-action (detail: { value, action }) to run them, and theme the parts via ::part(badge) / ::part(row-action).
For a flat set of collapsible sections rather than a hierarchy, reach for Accordion instead.

Props

4 props, straight from the manifest.

PropTypeDefaultBindingsDescription
items TreeNode[]
html svelte astro
The node hierarchy. Each `TreeNode` has a `label` and optional `value`, `href`, `children`, `expanded`, `selected`, and `disabled`. Each node can also take a `badge` (trailing content after the label: a plain string, a toned `{ text, tone }` pill, or a list of them so a count and a status pill each keep their own color) and `actions` (a `{ id, label, icon?, disabled? }[]` of hover-revealed trailing buttons, non-link rows only) that fire a `tree-action` event; a `disabled` action renders greyed and non-firing instead of being omitted, so a positional control set keeps a stable count. Passed as a property in the bindings (serialized to JSON for the element).
size Size
sm md lg
md
html svelte astro
Row density: `sm`, `md`, or `lg`.
label string
html svelte astro
Accessible name for the tree, applied as `aria-label`.
labelledby string
html svelte astro
Id of an external element naming the tree; takes precedence over `label`.

Appearance

Sizes

sm

.xtyle-tree--sm

Compact rows.

md

default
.xtyle-tree

Default.

lg

.xtyle-tree--lg

Roomy rows.

States

selected

.xtyle-tree__item[aria-selected="true"] > .xtyle-tree__row

The chosen node: its row takes the accent background and text. When the theme's --selection-cue resolves to marker (a high-contrast or redundant-cues algorithm), a non-color check glyph is added so selection never rests on color alone.

row-hover

.xtyle-tree__row:hover

Pointer over a row: the hover tint.

expanded

.xtyle-tree__item[aria-expanded="true"]

An open parent: the twisty rotates and the child group shows.

focus-visible

.xtyle-tree__item:focus-visible > .xtyle-tree__row

Keyboard focus on a node: an inset token ring on its row.

disabled

.xtyle-tree__item[aria-disabled="true"] > .xtyle-tree__row

A locked node: muted and non-interactive.

Anatomy

The named parts that make up the component, with their selectors.

tree

.xtyle-tree

The role="tree" root list holding the top-level nodes.

--font-sans --text-sm --fg-1

row

.xtyle-tree__row

A node's clickable row, indented by its level; a <div>, or an <a> when the node has an href.

--space-1 --space-2 --space-4 --radius-sm --fg-0 --fg-2 --state-hover --accent-bg --accent-text --weight-medium --border-normal --border-thick --ring --fg-disabled --duration-fast --ease-standard

twisty

.xtyle-tree__twisty

The disclosure caret on parent nodes; rotates 90° when the node is expanded, hidden on leaves.

--fg-3 --duration-fast --ease-standard

group

.xtyle-tree__group

A nested role="group" list of child nodes; hidden when its parent is collapsed.

trailing

.xtyle-tree__trailing

Per-row content after the label: one or more badge pills (a count plus a toned status, always shown) and hover-revealed actions buttons. Reachable at ::part(badge) and ::part(row-action).

--text-xs --fg-0 --fg-2 --fg-3 --state-hover --space-0

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 44/44 consumed tokens produced default register: 299 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent-2-text --accent-3-text --accent-4-text --accent-bg --accent-text --black-text --blue-text --border-normal --border-thick --brown-text --cyan-text --danger-text --duration-fast --ease-standard --fg-0 --fg-1 --fg-2 --fg-3 --fg-disabled --font-sans --gray-text --green-text --info-text --neutral-text --orange-text --pink-text --purple-text --radius-sm --red-text --ring --selection-cue --space-0 --space-1 --space-2 --space-4 --state-hover --success-text --text-body --text-sm --text-xs --warn-text --weight-medium --white-text --yellow-text

Accessibility

Builds the WAI-ARIA tree pattern: a role="tree" containing role="treeitem" nodes and nested role="group" levels, each item carrying aria-level, aria-selected, and (on parents) aria-expanded.
A single roving tab stop makes the whole tree one Tab stop; Up/Down move between visible nodes, Right expands or steps into a node, Left collapses or steps out to the parent, and Home/End jump to the first and last.
Enter or Space activates a node: selecting it, toggling a parent, or following a leaf's link.
A node marked disabled is announced and cannot be selected or toggled.
A locked branch with no href is a non-interactive section header: roving focus skips it and it can't be selected, so a keyboard user tabs straight to its children (a locked branch that carries an href stays a navigable header).
Focus shows an inset token ring on the node's row plus a transparent outline the forced-colors base rule promotes to a real system outline.
Selection carries a non-color channel on demand: when the theme sets --selection-cue: marker, the selected row gains a check glyph alongside the color, satisfying WCAG 1.4.1 (color is not the only differentiator). The algorithm decides. High-contrast emits marker by default, and any algorithm can opt in via the cues knob.
A row's actions are real <button>s with an accessible name (their label), revealed on row hover and keyboard focus; a badge is decorative (aria-hidden), so the row's accessible name stays the bare label. The action buttons are pointer- and screen-reader-reachable; they are not standalone Tab stops, matching the tree's single roving tab stop.

Code

A documentation tree

An expanded Guides branch with a selected page and links, beside a collapsed Reference branch.

<xtyle-tree label="Documentation"></xtyle-tree>

<script>
	document.querySelector("xtyle-tree").items = [
		{
			label: "Guides",
			expanded: true,
			children: [
				{ label: "Getting started", href: "/guides/start", selected: true },
				{ label: "Theming", href: "/guides/theming" },
			],
		},
		{
			label: "Reference",
			children: [{ label: "Engine", href: "/reference/engine" }],
		},
	];
</script>
<script lang="ts">
	import { Tree } from "@xtyle/svelte";

	const items = [
		{
			label: "Guides",
			expanded: true,
			children: [
				{ label: "Getting started", href: "/guides/start", selected: true },
				{ label: "Theming", href: "/guides/theming" },
			],
		},
		{
			label: "Reference",
			children: [
				{ label: "Engine", value: "engine", actions: [{ id: "rename", label: "Rename", icon: "✎" }] },
			],
		},
	];
</script>

<Tree
	label="Documentation"
	{items}
	onselect={(e) => console.log("select", e.detail.value)}
	ontreeaction={(e) => console.log(e.detail.action, "on", e.detail.value)}
/>
---
import { Tree } from "@xtyle/astro";

const items = [
	{
		label: "Guides",
		expanded: true,
		children: [
			{ label: "Getting started", href: "/guides/start", selected: true },
			{ label: "Theming", href: "/guides/theming" },
		],
	},
	{ label: "Reference", children: [{ label: "Engine", href: "/reference/engine" }] },
];
---

<Tree label="Documentation" items={items} />

Badges and row actions

A binder tree where each row carries a trailing badge (a count) and a hover cluster of action buttons, all firing tree-action.

<xtyle-tree label="Binder"></xtyle-tree>

<script>
	const tree = document.querySelector("xtyle-tree");
	tree.items = [
		{
			label: "Chapter 1", value: "ch1", expanded: true,
			// a word count plus a toned drift pill: two trailing badges, each its own color
			badge: ["1,204", { text: "3", tone: "warn" }],
			children: [
				{ label: "Opening", value: "ch1-1", badge: "512", actions: [
					{ id: "up", label: "Move up", icon: "↑", disabled: true },
					{ id: "rename", label: "Rename", icon: "✎" },
					{ id: "delete", label: "Delete", icon: "✕" },
				] },
				{ label: "Rising action", value: "ch1-2", actions: [{ id: "rename", label: "Rename", icon: "✎" }] },
			],
		},
		{ label: "Chapter 2", value: "ch2", badge: "812" },
	];
	// Badges show always; actions reveal on hover and fire tree-action (a disabled action stays greyed).
	tree.addEventListener("tree-action", (e) => console.log(e.detail.action, "on", e.detail.value));
</script>