Terracotta
GitHub

Use a Tree when each node is a single item, and a Grid when the rows do not nest.

import {
  TreeGrid,
  TreeGridBody,
  TreeGridCell,
  TreeGridColumnHeader,
  TreeGridFoot,
  TreeGridHead,
  TreeGridHeaderRow,
  TreeGridRow,
  TreeGridRowHeader,
  TreeGridStateChild,
  useTreeGridState,
} from 'terracotta/treegrid';

Anatomy

<TreeGrid>                  {/* <table role="treegrid">, owns expansion */}
  <TreeGridHead>            {/* <thead role="rowgroup"> */}
    <TreeGridHeaderRow>     {/* a header row; does not take focus */}
      <TreeGridColumnHeader/>
    </TreeGridHeaderRow>
  </TreeGridHead>
  <TreeGridBody>            {/* <tbody role="rowgroup"> */}
    <TreeGridRow value level expandable>{/* role="row", takes focus */}
      <TreeGridCell/>       {/* role="gridcell" */}
    </TreeGridRow>
  </TreeGridBody>
</TreeGrid>

The row group, header row and cell parts are the same components as the Grid parts, under treegrid names. TreeGridFoot is there too, for footer rows.

Examples

Basic

Declaring the hierarchy

The rows of a table are flat, so the treegrid cannot see which row is inside which. Declare it on each row:

  • level is the depth, from 1. It sets aria-level.
  • expandable marks a parent row. It gets aria-expanded. Rows without it never get aria-expanded, so they are not announced as parents.
  • Render a parent's child rows right after it, while it is expanded. Read that from the treegrid's render prop, <TreeGridStateChild> or useTreeGridState():
<TreeGrid aria-label="Inbox" defaultExpanded={['trip']}>
  {state => (
    <TreeGridBody>
      <TreeGridRow value="trip" expandable aria-label="Trip in June">…</TreeGridRow>
      {state.isExpanded('trip') && (
        <TreeGridRow value="trip-1" level={2} aria-label="Re: Trip in June">…</TreeGridRow>
      )}
    </TreeGridBody>
  )}
</TreeGrid>

Naming rows

A focused row is read by its name. Give each TreeGridRow an aria-label that sums it up, as the demo does. Otherwise a screen reader reads every cell.

Widgets in rows

The widgets of the row that has focus stay in the tab sequence. From a row, Tab moves through its buttons and links, then out of the treegrid, as the pattern describes. Widgets in other rows are reached with the arrow keys.

Controlled expansion

const [open, setOpen] = createSignal<string[]>([]);

<TreeGrid aria-label="Inbox" expanded={open()} onExpandedChange={setOpen}>…</TreeGrid>

State attributes

Element Attribute Present when
TreeGrid tc-treegrid Always
TreeGridRow tc-treegrid-row Always
TreeGridRow tc-level Always, set to the row's depth
TreeGridRow tc-expanded The row is expanded

Reading the state in code

On the treegrid, through its render prop, <TreeGridStateChild> or useTreeGridState():

Member Type Description
isExpanded(value) (V) => boolean Whether a row is open.
setExpanded(value, open) (V, boolean) => void Opens or closes a row.
toggle(value) (V) => void Flips a row.

On a row, through its render prop: level(), isExpandable(), isExpanded() and toggle().

Keyboard

On a focused row:

Key Action
→ Opens a closed parent row. Otherwise, moves to the first cell.
← Closes an open row. Otherwise, focus stays.
↓ / ↑ Next / previous row
Page Down / Page Up Moves pageSize rows
Home / End First / last row
Enter Opens or closes a parent row
Tab Moves through the widgets in the row, then out

On a focused cell:

Key Action
→ / ← Next / previous cell. ← on the first cell moves to the row.
↓ / ↑ Cell below / above, header row included
Page Down / Page Up Moves pageSize rows in the column
Home / End First / last cell in the row
Ctrl+Home / Ctrl+End Cell in the first / last row of the column

As in Grid, focus lands on a cell, or on the one widget inside it, and a text field in a cell keeps its own arrow keys. The pattern's optional selection keys are not included.

API

<TreeGrid>

Renders a <table> by default, with role="treegrid".

Prop Type Default Description
as ValidConstructor 'table' Element or component to render as.
defaultExpanded V[] [] Values of the rows that start open.
expanded V[] none Values of the open rows, controlled.
onExpandedChange (expanded: V[]) => void none Called with the new open rows.
by (a: V, b: V) => boolean reference equality Compares row values.
pageSize number 5 Rows moved by Page Up and Page Down.
children JSX.Element | (state: TreeGridStateProperties<V>) => JSX.Element none Contents, or a render prop.
…rest props of as none Forwarded to the rendered element.

<TreeGridRow>

Renders a <tr> by default.

Prop Type Default Description
value V none Identifies the row. Required.
level number 1 The depth of the row.
expandable boolean false Marks a parent row.
children JSX.Element | (row: TreeGridRowProperties) => JSX.Element none Cells, or a render prop.

Rendered attributes: role="row", aria-level, aria-expanded on parent rows, tabindex.

Row groups, header rows and cells

These are the Grid parts under treegrid names:

Component Renders Role
<TreeGridHead> <thead> rowgroup
<TreeGridBody> <tbody> rowgroup
<TreeGridFoot> <tfoot> rowgroup
<TreeGridHeaderRow> <tr> row, which does not take focus
<TreeGridCell> <td> gridcell
<TreeGridColumnHeader> <th> columnheader
<TreeGridRowHeader> <th> rowheader