Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEComposite RolesHybrid widgetExpandable rowsSpreadsheet + hierarchy

treegrid

Combines hierarchical structure (tree) with columns (grid). Users can expand/collapse rows while retaining columnar data and keyboard navigation.

Live Example

Budget Treegrid

Hierarchy plus tabular data with expand/collapse controls and trend indicators.

Dept.
Budget
Change
Marketing
$250k
+12%
Brand
$150k
+4%
Growth
$100k
+8%
Product
$420k
+5%

Purpose

Display drill-down hierarchies with tabular information (e.g., financial rollups).

Structure

Rows may own children inside role="rowgroup". Cells behave like gridcells.

Screen Reader

Announces both coordinates and hierarchy level. Users expect tree + grid commands.

When to Use

1

Financial statements

Parent rows sum child rows with expand/collapse.

2

Project portfolios

Show initiatives with nested tasks plus metrics columns.

3

Access control lists

Reveal nested permissions plus per-role columns.

Required Structure

Row composition

role="row" plus aria-level/setsize/posinset describes the hierarchy.

Expandable rows

Rows that own children toggle aria-expanded and wrap kids in role="rowgroup".

Cells

role="gridcell" for each data cell. Use aria-describedby to connect headers.

Keyboard Interaction Model

ActionKeysResult
Move horizontallyArrowLeft / ArrowRightMoves between gridcells in the same row.
Move verticallyArrowUp / ArrowDownMoves between rows, staying in the same column.
Expand/collapseArrowRight / ArrowLeft on first columnExpands or collapses the focused row if children exist.
Jump to edgesHome / EndMoves to first/last cell of the current row.
Jump levelsCtrl + Home / Ctrl + EndMoves to the first/last visible row.

Required States & Properties

aria-levelOptional

Depth of the row within the hierarchy.

aria-expandedOptional

Applied to rows that have children to indicate whether they are visible.

aria-setsizeOptional

Number of sibling rows at the same hierarchy level.

aria-posinsetOptional

Position of the row inside its current set.

aria-rowcount / aria-colcountOptional

Communicate total rows/columns especially when virtualizing.

aria-activedescendantOptional

Optional alternative to moving DOM focus for virtualized grids.

Implementation Checklist

  • Clearly identify which column contains the expand/collapse toggles and keep focus on that cell when toggling.

  • When rows collapse, keep descendants in the DOM but hidden so aria relationships remain valid.

  • Announce summary values or totals with aria-live if expanding recalculates metrics.

  • Provide keyboard shortcuts or buttons to expand/collapse all nodes for faster navigation.

Code Examples

Treegrid Rows with Levels

Manual example showing aria-level on rows.

<div role="treegrid" aria-rowcount="4" aria-colcount="3">
  <div role="row" aria-level="1" aria-expanded="true">
    <div role="gridcell">Marketing</div>
    <div role="gridcell">$250,000</div>
    <div role="gridcell">32%</div>
  </div>
  <div role="rowgroup">
    <div role="row" aria-level="2" aria-posinset="1" aria-setsize="2">
      <div role="gridcell">Brand</div>
      <div role="gridcell">$140,000</div>
      <div role="gridcell">18%</div>
    </div>
    <div role="row" aria-level="2" aria-posinset="2" aria-setsize="2">
      <div role="gridcell">Product</div>
      <div role="gridcell">$110,000</div>
      <div role="gridcell">14%</div>
    </div>
  </div>
</div>

React Expandable Row

Row component toggles aria-expanded and conditionally renders children.

function TreeRow({ node, level = 1 }) {
  const [open, setOpen] = useState(false)
  const hasChildren = Boolean(node.children?.length)

  return (
    <>
      <div
        role="row"
        aria-level={level}
        aria-expanded={hasChildren ? open : undefined}
      >
        <div role="gridcell">
          {hasChildren && (
            <button
              aria-label={open ? 'Collapse row' : 'Expand row'}
              onClick={() => setOpen((prev) => !prev)}
            >
              {open ? '▾' : '▸'}
            </button>
          )}
          {node.label}
        </div>
        <div role="gridcell">{node.budget}</div>
        <div role="gridcell">{node.share}</div>
      </div>
      {hasChildren && open && (
        <div role="rowgroup">
          {node.children.map((child) => (
            <TreeRow key={child.id} node={child} level={level + 1} />
          ))}
        </div>
      )}
    </>
  )
}

Best Practices

🧱

Align toggles

Keep expanders in the same column to reduce cognitive load.

📣

Summaries

Show summary rows for collapsed parents so users know totals without expanding.

🪄

Progressive disclosure

Lazy-load children and indicate loading with aria-busy.

Common Mistakes

Breaking aria-level chain

Forgetting to increment aria-level causes screen readers to report wrong hierarchy.

Calculate level relative to parent and keep values up to date.

Removing collapsed kids from DOM

Destroying child rows resets focus and aria relationships.

Keep rowgroup elements in DOM with hidden attribute.

Related Roles & Patterns