Skip to content

Tree view component

components specs/components/tree-view.kmd

Hierarchical, expandable/collapsible list for nested data — file explorers, settings navigation, nested categories, org structures — with optional tri-state cascading checkboxes, lazy child loading, and full keyboard + ARIA tree semantics. Modeled after Fluent 2 TreeView (and the WAI-ARIA tree pattern). Distinct from a flat list and from a data table.

When this spec applies

Primary triggers

All triggers

Specification body

Component — Tree view

Status: v0.1.0 — Draft. Promoted from the Microsoft Fluent 2 parity scan (meta/docs/stack #081). Sources: https://fluent2.microsoft.design/ (TreeView) + WAI-ARIA tree pattern.

R1 — When to use

Use a tree view for data with a parent→child hierarchy the user navigates by expanding nodes (files/folders, nested settings, categories). Do NOT use it for:

  • flat, same-level items → specs/components/lists.kmd;
  • tabular/columnar records → specs/components/data-table.kmd;
  • transient command lists → specs/components/menus.kmd.

R2 — Anatomy

Each node row: an expand/collapse twisty (only when it has children), depth indentation, an optional checkbox (R4), an optional icon, and the label. Leaf nodes align with their siblings (no orphan twisty).

R3 — Expand / collapse

  • Children may load lazily on first expand (show a specs/components/skeleton.kmd placeholder while loading).
  • Expansion state persists across re-render within a session; expand-all / collapse-all are optional bulk controls.
  • A node with zero children after load shows no twisty.

R4 — Cascading checkboxes (tri-state)

When selection-by-inclusion is enabled, checkboxes are tri-state:

  • checking a node checks all descendants;
  • unchecking clears all descendants;
  • a node whose descendants are mixed renders indeterminate (aria-checked="mixed");
  • a node becomes checked iff all its children are checked (state propagates up as well as down). The checked set is derived from leaves; never store a parent "checked" that contradicts its children.

R5 — Selection vs check (distinct)

  • Focus/selection = the active node (single, keyboard-navigable).
  • Check = membership in an inclusion set (R4), independent of focus. Both can coexist (you can move focus without changing checks). For pure multi-select without hierarchy semantics, see specs/interaction/selection.kmd.

R6 — Keyboard

  • ↑/↓ move focus across visible nodes; expands (or moves to first child), collapses (or moves to parent); Home/End jump to first/last; type-ahead matches labels.
  • Space toggles the focused node's checkbox; Enter activates the node's primary action.
  • Exactly one node is in the tab order (roving tabindex); arrows move within.

R7 — Scale

Virtualize rendering for large/deep trees (only expanded, on-screen nodes in the DOM). Any truncated/lazy-unloaded branch is disclosed, never silently capped.

R8 — Accessibility

  • role="tree" on the container; role="treeitem" per node; child lists wrapped in role="group".
  • Each item carries aria-expanded (when expandable), aria-selected, aria-checked (true|false|mixed for R4), and aria-level / aria-setsize / aria-posinset.
  • Indentation/twisty are not the sole structure signal — the ARIA level + group convey hierarchy to assistive tech.

Não-escopo

  • Drag-to-reorder / drag-between-nodes (separate enhancement).
  • Columnar tree-grid (a data-table variant, not this).
  • The backing data model / lazy-load transport.

Requirements (testable)

Requirement: Expand and collapse nodes {#req-tree-view-expand-collapse}

An expandable node SHALL toggle the visibility of its children via its twisty, SHALL load children lazily on first expand while showing a skeleton placeholder, and SHALL remove its twisty when it resolves to zero children. Leaf nodes SHALL NOT render a twisty.

Scenario: Expanding a collapsed node reveals its children

  • GIVEN a tree with a collapsed node that has children
  • WHEN the user activates that node's expand/collapse twisty
  • THEN the node's children become visible in the tree
  • AND the node's aria-expanded attribute is true

Scenario: First expand loads children lazily with a skeleton

  • GIVEN a node whose children have not yet been loaded
  • WHEN the node is expanded for the first time
  • THEN a skeleton placeholder is shown in the child region while loading
  • AND the loaded children replace the skeleton once the transport resolves

Scenario: A node with no children shows no twisty

  • GIVEN an expandable node whose lazy load resolves to zero children
  • WHEN the load completes
  • THEN the node renders no expand/collapse twisty
  • AND the node aligns with its leaf siblings

Requirement: Cascading tri-state checkboxes {#req-tree-view-tristate-checkboxes}

When selection-by-inclusion is enabled, checking a node SHALL check all its descendants, unchecking SHALL clear all descendants, a node with mixed descendants SHALL render indeterminate with aria-checked="mixed", and a node SHALL become checked if and only if all of its children are checked.

Scenario: Checking a parent checks all descendants

  • GIVEN a node with multiple unchecked descendant nodes
  • WHEN the user checks that node
  • THEN every descendant of the node becomes checked
  • AND the node's aria-checked is true

Scenario: A partially-checked parent renders indeterminate

  • GIVEN a node with some descendants checked and some unchecked
  • WHEN the checkbox states settle
  • THEN the parent node renders as indeterminate
  • AND the parent node's aria-checked is mixed

Scenario: Checking the last child promotes the parent to checked

  • GIVEN a node whose children are all checked except one
  • WHEN the user checks that remaining child
  • THEN the parent node becomes checked
  • AND the parent node's aria-checked is true

Requirement: Selection and check are independent {#req-tree-view-selection-vs-check}

Focus/selection (the single active node) and check (membership in the inclusion set) SHALL be independent: moving focus SHALL NOT alter any node's checked state, and toggling a check SHALL NOT change which node holds focus.

Scenario: Moving focus does not change checks

  • GIVEN a tree where one node is checked and a different node has focus
  • WHEN the user moves focus to another node without toggling any checkbox
  • THEN the checked set is unchanged
  • AND the previously checked node remains checked

Scenario: Toggling a check does not move focus

  • GIVEN a focused node in the tree
  • WHEN the user toggles a checkbox on a different node
  • THEN focus remains on the originally focused node
  • AND only the toggled node's checked state changes

Requirement: Keyboard navigation and roving tab order {#req-tree-view-keyboard}

The tree SHALL support arrow-key navigation where Down/Up move focus across visible nodes, Right expands or moves to the first child, Left collapses or moves to the parent, Home/End jump to the first/last node, Space toggles the focused node's checkbox, and Enter activates its primary action; exactly one node SHALL be in the tab order via roving tabindex.

Scenario: Right arrow expands then descends

  • GIVEN focus on a collapsed expandable node
  • WHEN the user presses the Right arrow key
  • THEN the node expands and its aria-expanded becomes true
  • AND pressing Right again moves focus to the node's first child

Scenario: Space toggles the focused node's checkbox

  • GIVEN focus on a node with an unchecked checkbox
  • WHEN the user presses the Space key
  • THEN the focused node's checkbox becomes checked
  • AND focus remains on that node

Scenario: Only one node is tabbable

  • GIVEN a rendered tree with multiple visible nodes
  • WHEN the user tabs into the tree
  • THEN exactly one node carries tabindex="0" and all other nodes carry tabindex="-1"
  • AND arrow keys move focus without leaving the tree

Requirement: ARIA tree semantics {#req-tree-view-aria}

The container SHALL expose role="tree", each node SHALL expose role="treeitem", child lists SHALL be wrapped in role="group", and every node SHALL carry aria-level, aria-setsize, and aria-posinset so hierarchy is conveyed to assistive tech independently of visual indentation.

Scenario: Roles and positional attributes are exposed

  • GIVEN a rendered tree with a nested child group
  • WHEN assistive tech inspects the accessibility tree
  • THEN the container has role="tree" and each node has role="treeitem"
  • AND each nested child list is wrapped in an element with role="group"
  • AND each node exposes aria-level, aria-setsize, and aria-posinset

Scenario: Hierarchy is conveyed without relying on indentation

  • GIVEN a node nested three levels deep
  • WHEN the node's ARIA attributes are read
  • THEN its aria-level reflects its depth in the hierarchy
  • AND the level is present regardless of the visual indentation or twisty

References