Skip to content
Glint UI

TreeView

Hierarchical navigation over a flattened row model, virtualized and walked entirely from the keyboard.

Usage

import { TreeNode, TreeView } from "@glint/components/tree-view.slint";
import { Tokens } from "@glint/theme/tokens.slint";
export component AppWindow inherits Window {
width: 360px;
height: 400px;
background: Tokens.color-background;
// Every node whose ancestors are all expanded, in display order.
in property <[TreeNode]> visible-rows;
in-out property <string> selected-id;
callback set-expanded(string, bool);
callback open(string);
TreeView {
accessible-label: "Project files";
nodes: root.visible-rows;
selected <=> root.selected-id;
toggled(id, expanded) => { root.set-expanded(id, expanded); }
activated(id) => { root.open(id); }
}
}

A tree here is not nested elements. Slint cannot instantiate a component recursively, and a virtualized row cannot own a slot, so the tree is a flat array of rows — each carrying the depth that draws its indent and the parent that Left climbs to. The rows you hand over are exactly the rows the tree draws: a collapsed branch’s descendants are absent from nodes, not hidden inside it, because a virtualized list pays for the rows it is given (ADR-0016). Flattening is therefore yours, the same trade DataTable makes for sorting and paging.

TreeNode is a data type rather than a component:

FieldWhat it is
labelWhat the row shows and what a screen reader announces
idWhat selected, toggled and activated speak in
depthNesting level, 0 at the top — it draws the indent
parentIndex of the row this one hangs under (-1 at the top level)
expandableMarks a branch: it gets a chevron and answers the expand action
expandedWhether its children follow it in the model

selected is the state and activated is the event that changed it — the tree keeps a selection the way Sidebar keeps an active row, rather than firing a one-shot pick the way Command’s selected(id) does. The tree never flips expanded itself either: the shape of the model stays yours.

Examples

Selection

A click, Enter or Space commits the highlighted row into the selection: selected updates first, then activated(id) fires. Compare selected against nodes[i].id.

Indent and row height

indent is the horizontal offset one nesting level adds, and row-height is both the height of a row and the step the highlight scrolls by. The chevron slot is held whether or not a row has one, so leaves and branches line up down a level.

A tree taller than its slot

The rows are virtualized through ListView: the tree is content-sized while the surrounding layout leaves it free, and scrolls once the layout constrains its height — the large-model case. Walking past the bottom edge with Down scrolls the highlight into view rather than losing it.

Starting the keyboard on the selection

highlighted-index is the row the keyboard stands on; committing it is what selects. Slint cannot search nodes for an id, so the tree cannot start the keyboard on a selection it was handed — you built the array, so you know the index. Set the two together.

API Reference

Properties

PropertyTypeDefaultDescription
nodesin [TreeNode]no defaultThe visible rows, in display order: every node whose ancestors are all expanded. The consumer owns the flattening.
selectedin-out stringno defaultTwo-way; id of the selected row. Compare against nodes[i].id.
highlighted-indexin-out int0Row the keyboard stands on. Committing it (Enter, Space, a click) is what selects. A click brings it along, so the one time to set it is alongside a selected the consumer assigns itself: Slint cannot search nodes for an id, so the tree cannot start the keyboard on a selection it was handed — the consumer, who built the array, knows the index.
row-heightin length32pxHeight of one row; also the step the highlight scrolls by.
indentin lengthTokens.spacing-lgHorizontal offset one nesting level adds.

Callbacks

CallbackDescription
toggled(string, bool)Fired with a row's id and the expansion it asks for. The tree never flips expanded itself: the shape of the model stays the consumer's.
activated(string)Fired with a row's id when the user selects it; selected updates first.

Accessibility

  • Tree role. The root carries Slint’s tree role and the whole row count; naming it is the call site’s, so set accessible-label.
  • Rows are selectable, expandable items. Each row is a list-item with its index, its label, and — on a branch — accessible-expandable and accessible-expanded. Selecting it is its default action and opening or closing it is its expand action, so assistive technology can do either without doing both.
  • The level is spelled out. Slint 1.17 has no accessible level, so each row carries “Level 2” as its description — otherwise the hierarchy the indent draws would be visual only.
  • Where the platform stops. The tree role does reach both real backends (winit maps it to AccessKit’s Role::Tree, Qt to QAccessible::Tree). What is missing is below the root: there is no tree-item role, so a row can only be a list item. A screen reader therefore hears a tree of expandable list items that each say which level they are on, rather than a tree whose items carry their level structurally.
  • Keyboard. The tree is one tab stop. Up / Down walk the rows and Home / End reach the ends, all clamped — a tree does not wrap. Right opens a closed branch and, once open, steps into it; Left closes an open branch and otherwise climbs to the parent. Enter and Space commit the highlight into the selection.
  • The highlight is not the selection. A tree nobody has focused does not sit there with row 0 lit up competing with the selection: the highlight is drawn only while the tree holds the keyboard. A click brings it along.
  • Keyboard-only focus ring, around the row the arrows landed on rather than around the whole tree.