Skip to main content
Search
Search the documentation

Tree view

A hierarchy the keyboard can walk - the WAI-ARIA tree pattern, from a plain array.

pnpm dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-tree-view.json

Nested <ul>s with a chevron that toggles a boolean is a tree for the mouse. The keyboard gets nothing, or a tab stop per row, which in a folder of two hundred files is two hundred Tab presses to get past it. A screen reader hears a list of lists with no idea what is open.

src
README.md

Selected: —

What Coral adds

  • One tab stop: the tree is reached with Tab once; inside it, the arrows move.
  • The whole keyboard pattern: Up and Down move; Right opens a node, then steps into it; Left closes it, then steps out to the parent; Home and End; Enter and Space select; * opens every sibling; typing letters jumps to the next matching row, ignoring case and accents.
  • Announced structure: aria-level, aria-posinset, aria-setsize and aria-expanded on every row: "src, expanded, level 1, 1 of 3".
  • Focus never lost. Collapsing a folder that contains the focused row moves focus to the folder.
  • Disabled nodes stay reachable. They cannot be selected, but can still be read and expanded.
  • Right-to-left aware. Left and Right swap, and the chevron flips.

Loading children on demand

A node with children: [] is a branch with nothing loaded yet: it can be opened. Fetch when its id appears in onexpandedchange, then hand back nodes with the children filled in.

Data

type TreeNode<T = unknown> = {
	id: string; // unique across the whole tree
	label: string; // shown, and matched by typeahead
	children?: TreeNode<T>[]; // omit for a leaf; [] for an empty branch
	disabled?: boolean;
	data?: T; // yours, handed back in onselect and the snippet
};

Import

<script lang="ts">
	import TreeView from '#lib/components/coral/kit/tree-view/tree-view.svelte';
</script>

Props

Everything a <div> accepts stays available on the root. On top of that:

PropTypeDefaultDescription
nodesTreeNode<T>[]-The top level. Required.
expandedstring[][]Bindable. Ids of the open nodes.
selectedstring-Bindable. Id of the selected node.
onselect(node: TreeNode<T>) => void-Click, Enter or Space. Never for disabled nodes.
onexpandedchange(expanded: string[]) => void-Nodes opened or closed.
labelstring-Names the tree. Or use aria-labelledby.
localestring-Typeahead's case- and accent-insensitive matching.
toggleLabel(node, expanded) => string-Tooltip on the chevron.
classstring-Merged onto the root. [--coral-indent:1.5rem].
rowClassstring-Merged onto every row.
nodeSnippet<[NodeContext<T>]>-Replaces the row body.
iconSnippet<[NodeContext<T>]>-Replaces the chevron.

Rows expose data-state="open|closed", data-selected and data-disabled for styling.

Accessibility

Rows are rendered flat, each carrying its level, position and set size, rather than as nested role="group"s. Assistive tech announces the same structure from those attributes, and a flat list lets the focus ring sit on one row instead of ringing everything below it. The chevron is a mouse target hidden from assistive tech: a second focusable control inside a tree item would break the one-tab-stop model the pattern depends on.