Components / structure

Tree

Its items carry a branch/leaf marker. Treegrid's rows did not until the upstream port found it, which is the kind of asymmetry only a second implementation of the same idea surfaces.

Rendered from the package · open on its own

Summary

A hierarchy the reader expands and selects from — a file listing, a section outline.

APG patternTree View
Built onnative elements
Statusstable
Since0.1.0
<Tree label="Sections" defaultExpanded={['work']} selectedId={selected} onSelect={setSelected} nodes={…} />

Anatomy

A data-se="tree" list where each treeitem may hold a nested group.

PartElementRequired
group<ul>no
icon<span>no
item<div>yes
twisty<svg>yes

Properties

PropertyValuesDefaultWhat it does
nodesTreeNode[]requiredThe hierarchy, nested by children. A node with no children is a leaf, which is what the arrow keys treat differently.
labelstringrequiredNames the tree.
defaultExpandedstring[]noneWhich branches start open, by id. Everything else starts closed.
selectedIdstringnoneThe selected node. Selection and focus are separate here — arrowing moves focus without selecting.
onSelect(id: string) => voidnoneCalled with the id of the node the reader selected.

States

StateExpressed as
Focused:focus-visible
Hover:hover
Expandedaria-expanded
Selectedaria-selected

Behavior

The keyboard model is the pattern, and it is asymmetric on purpose. Right opens a closed branch but steps into an already-open one; Left closes an open branch but steps out of a leaf. That is what makes a deep tree navigable without a pointer, and it is the part most implementations get wrong.

Type-ahead moves to the next node starting with the letter. * opens every sibling branch at the same level.

Accessibility

One tab stop for the whole tree, with roving focus inside it.

No aria-level, aria-posinset or aria-setsize. The APG requires them only when the DOM does not already carry the set, and this anatomy nests ul[role="group"] inside the li it belongs to — so the structure says it and the attributes would be a second copy to keep in sync. A Treegrid is a flat table, so there they are required, and there they are emitted.

Keyboard

KeyResult
Arrow Down / Arrow UpMove to the next or previous visible node
Arrow RightOpens a closed branch; steps into an already-open one
Arrow LeftCloses an open branch; steps out of a leaf
Home / EndJump to the first or last visible node
EnterSelects the focused node
*Opens every sibling branch at the same level
a letterType-ahead

Roles, states and properties

AttributeSet by
aria-expandedset at render
aria-hiddenalways true
aria-labelset at render
aria-selectedset at render
rolealways treeitem, always group, always tree

Focus

One tab stop with roving focus inside. The asymmetry in the arrow keys is the pattern, and it is the part most implementations get wrong.

Screen reader

Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.

Contrast and target size

npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.

It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.

Tested

  • test/apg/batch-b.test.jsx
  • test/apg/batch-e.test.jsx
  • test/apg/partials-listbox.test.jsx
  • test/browser/icon-box.spec.mjs
  • test/browser/keyboard.spec.mjs
  • test/browser/site.spec.mjs
  • test/browser/upstream-composites.spec.mjs

Known gaps

  • 7 upstream refs — APG's own page or markup, not the pattern
  • 4 upstream refs — a navigation treeview; the rest is their page chrome
  • 1 upstream ref — APG requires level/posinset/setsize only when the DOM does not already carry the set. Ours nests ul[role="group"] inside the li it belongs to, so the structure says it and the attributes would be a second copy to keep in sync.
  • 2 upstream refs — see treeitem-aria-level

Appearance

TokenUsed for
--se-control-h-smmin-block-size
--se-duration-fasttransition
--se-ease-standardtransition
--se-focus-coloroutline
--se-focus-widthoutline, outline-offset
--se-icon-smblock-size, font-size, inline-size
--se-radius-smborder-radius
--se-space-2gap
--se-space-3padding-inline-end, padding-inline-start
--se-space-4padding-inline-start
--se-surface-selectedbackground
--se-surface-sunkbackground
--se-target-minmin-block-size
--se-textcolor
--se-text-mutedcolor
--se-text-smfont-size
--se-weight-mediumfont-weight

Motion

None. A branch that animates open delays a reader who is navigating by keyboard at speed.

Content

Node labels are names, not paths. The hierarchy is drawn; repeating it in the text doubles the reading.

Usage

Only if your domain is hierarchical. The keyboard model is expensive to learn and expensive to keep right, and a two-level tree is a list with headings.

When not to use it. If the rows carry columns of data — that is a Treegrid.

Notes

Its items carry a branch/leaf marker. Treegrid's rows did not until the upstream port found it, which is the kind of asymmetry only a second implementation of the same idea surfaces.