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
- Build a tree view in a Koder surface
All triggers
- Display nested/hierarchical data with expand/collapse
- Add a file-explorer or nested-settings navigation
- Select a subtree with cascading (tri-state) checkboxes
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.kmdplaceholder 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 inrole="group".- Each item carries
aria-expanded(when expandable),aria-selected,aria-checked(true|false|mixedfor R4), andaria-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-tablevariant, 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-expandedattribute istrue
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-checkedistrue
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-checkedismixed
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-checkedistrue
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-expandedbecomestrue - 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 carrytabindex="-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 hasrole="treeitem" - AND each nested child list is wrapped in an element with
role="group" - AND each node exposes
aria-level,aria-setsize, andaria-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-levelreflects its depth in the hierarchy - AND the level is present regardless of the visual indentation or twisty
References
specs/components/lists.kmdspecs/interaction/selection.kmdspecs/components/skeleton.kmdspecs/i18n/contract.kmd