Skip to content

Tree

Tree<T> is a focusable, virtual-scrolling outline: it takes a forest of TreeNode roots, flattens the currently-visible (expanded) nodes into an ordered row list, and paints only its visible window — so it stays fast over large trees. Nodes carry │├└─ connector guides and a + / expand marker, and it owns a vertical ScrollBar.

Expand state is owned by the view, keyed on node object identity — the node data stays plain and immutable, and the same node can live in more than one tree with independent expand state.

Usage

ts
import { Tree, signal } from '@jsvision/ui';
import type { TreeNode } from '@jsvision/ui';

// A leaf is `children: []`.
const n = (value: string, children: TreeNode<string>[] = []): TreeNode<string> => ({ value, children });
const roots = signal<TreeNode<string>[]>([n('src', [n('index.ts'), n('engine', [n('buffer.ts')])]), n('README.md')]);

const tree = new Tree({
  roots,
  getText: (name) => name,
  command: 'open',
  markerStyle: 'brackets', // `[+]`/`[-]` markers instead of the default `+`/`─`
});
tree.layout = { position: 'absolute', rect: { x: 0, y: 0, width: 28, height: 10 } };
// loop.focusView(tree.rows) — focus the rows renderer, not the group. tree.expandAll() to open all.

Live example

🚧 Live demo coming soon

An interactive, in-browser demo of Tree lands in a later update. Until then, the reference below covers its full API.

Props

new Tree(options).

PropTypeDefaultDescription
rootsSignal<TreeNode<T>[]>The reactive forest of root nodes (a single-root tree is the 1-element case).
getText(value: T) => stringRender a node's value to its row text.
focusedSignal<number>internal 0The highlighted flattened-visible index.
selectedSignal<number>internal −1The chosen flattened-visible index (-1 = none).
onSelect(index, node) => voidActivation callback (Enter / text double-click).
commandstringCommand emitted on activation, handled elsewhere.
expandedByDefaultbooleanfalseSeed every node with children as expanded at construction.
guidesbooleantrueDraw the │├└─ connectors; false = flat indent.
markerStyle'tv' | 'brackets' | 'triangle''tv'Expand-marker style (see below).

Keyboard & mouse

InputResult
↑ / ↓Move the highlight.
PgUp / PgDnPage the highlight.
Home / EndJump to the first / last visible row.
Ctrl+PgUp / PgDnJump to the ends.
+ / −Expand / collapse the focused node.
*Expand the focused node's whole subtree.
← / →Collapse-or-parent / expand-or-child.
EnterActivate (fires onSelect / emits command).
Click a guideToggle that node.
Double-click textActivate that node.

Sizing & layout

Give the Tree its own bounds (an absolute rect or a flex slot); internally it lays out as a row — the rows renderer fills the width, the owned scroll bar takes the rightmost column. Because a plain Group is not itself a focus target, focus the exposed tree.rows, not the tree.

The marker style adapts to the terminal: 'tv' is a faithful single +/, 'brackets' draws [+]/[-] (pure ASCII, the most legible), and 'triangle' draws / but falls back to 'brackets' where Unicode is unavailable. Only the marker column changes — indentation and connectors are identical across styles.

Best practices

  • Keep node data plain. Expand state lives in the view, keyed on identity — never mutate the TreeNode objects to track open/closed. Drive expansion with expand / collapse / toggle / expandAll / collapseAll / expandSubtree.
  • Focus tree.rows. The group is not focusable; focusing it does nothing.
  • Pick a marker for your audience. 'brackets' is the safest for legibility and ASCII terminals; keep the default 'tv' for a classic look.

Theming

RoleApplies to
outlineNormalA normal row (expanded node or leaf): yellow on blue
outlineFocusedThe focused row — an inverted bar: blue on lightGray
outlineSelectedA selected row: brightGreen on blue
outlineNotExpandedA collapsed node's text (a hint it has children)

The owned scroll bar uses the scrollBar* roles.