Components / structure
Treegrid
Sharing data-se="grid" with DataGrid is the anatomy being shared rather than a name collision: two components, one stylesheet, and the roles stay distinct.
Summary
A grid whose rows form a hierarchy — a file listing with sizes, a nested budget.
| APG pattern | Treegrid |
| Built on | native elements |
| Status | stable |
| Since | 0.1.0 |
<Treegrid label="Files" defaultExpanded={['images']}
columns={[{ key: 'name', label: 'Name' }, { key: 'size', label: 'Size' }]} rows={…} />
Anatomy
A table carrying role="treegrid". It renders data-se="grid" so one set of rules dresses it and DataGrid alike.
| Part | Element | Required |
|---|---|---|
cell | <td> | yes |
columnheader | <th> | yes |
row | <tr> | yes |
twisty | <svg> | yes |
twisty-button | <button> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
columns | TreegridColumn[] | required | The columns, in display order. The first carries the name and the hierarchy. |
rows | TreegridRow[] | required | The rows, nested by children. Unlike a Tree, the nesting is not in the DOM — a treegrid is a flat table, which is why every row carries its own level. |
label | string | required | Names the grid. |
defaultExpanded | string[] | none | Which rows start expanded, by id. |
pageSize | number | 10 | Rows moved by Page Up and Page Down. APG leaves this to the author. |
States
| State | Expressed as | |
|---|---|---|
| Focused | :focus-visible | |
| Selected | aria-selected |
Behavior
The two-dimensional grid keyboard plus the tree's expand rules, and the join is the hard part. Right expands a collapsed row when focus is in the first column, but moves a cell everywhere else. Left collapses, or moves back a cell.
Home and End move within the row; Ctrl with either moves within the grid and keeps the column.
Accessibility
Rows carry aria-level, aria-posinset and aria-setsize, unlike Tree. A treegrid is a flat table — the DOM does not carry the hierarchy, so the attributes are the only place it exists.
Branch rows carry an expanded marker. They did not until the upstream port found it, though Tree items always had one.
One tab stop for the whole grid.
Keyboard
| Key | Result |
|---|---|
Arrow keys | Move one cell in two dimensions |
Arrow Right | Expands a collapsed row when focus is in the first column; moves a cell everywhere else |
Arrow Left | Collapses, or moves back a cell |
Home / End | Jump within the row |
Ctrl + Home / End | Jump within the grid, keeping the column |
Page Up / Page Down | Move by a screen |
Enter | Activates the cell |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-expanded | set at render |
aria-hidden | always true |
aria-label | set at render |
aria-level | set at render |
aria-posinset | set at render |
aria-setsize | set at render |
role | always treegrid, always row, always columnheader, always gridcell |
Focus
One tab stop. The join between the grid keyboard and the tree's expand rules is the hard part — the same arrow means two things depending on the column.
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 sets no target size of its own; anything interactive inside it brings its own.
Tested
test/apg/batch-c.test.jsxtest/apg/batch-e.test.jsxtest/browser/axe.spec.mjstest/browser/keyboard.spec.mjstest/browser/site.spec.mjstest/browser/upstream-combobox.spec.mjstest/browser/upstream-composites.spec.mjstest/browser/upstream-forms-menubar.spec.mjstest/browser/upstream-overlays.spec.mjstest/browser/upstream-toolbar-tree.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-accent-soft | background |
--se-border | border, border-block-end |
--se-border-width | border, border-block-end |
--se-focus-color | outline |
--se-focus-width | outline, outline-offset |
--se-radius-md | border-radius |
--se-space-2 | gap, padding |
--se-space-3 | padding |
--se-space-4 | padding-inline-start |
--se-surface-selected | background |
--se-surface-sunk | background |
--se-text | color |
--se-text-muted | color |
--se-text-sm | font-size |
Motion
None.
Content
The first column carries the name and the hierarchy; the rest carry values. A column whose meaning depends on the row's depth belongs in the first one.
Usage
When not to use it. If the rows carry no columns, that is a Tree. If they carry columns but no hierarchy, that is a DataGrid — or a DataTable if the cells are only read.
Notes
Sharing data-se="grid" with DataGrid is the anatomy being shared rather than a name collision: two components, one stylesheet, and the roles stay distinct.