Grouping by Multiple Columns

Permalink to "Grouping by Multiple Columns"

Grouping by one column is a header row and some children. Grouping by two or three columns turns a flat table into a tree: Region › Status › Owner, with subtotals at every level. Sighted users read the indentation at a glance. Screen reader users need that structure spelled out — which level they are on, which parent groups they are inside, how many rows each group holds — and a way to change the grouping without a drag-and-drop “group by” bar.

This guide belongs to row grouping and aggregation.

Spec reference

Permalink to "Spec reference"
  • ARIA treegrid — aria-level, aria-setsize, aria-posinset and aria-expanded on group rows convey nesting.
  • SC 1.3.1 Info and Relationships (A) — group membership and level must be programmatic, not just indentation.
  • SC 2.5.7 Dragging Movements (AA) — drag-to-group bars need a non-drag alternative.
  • SC 4.1.3 Status Messages (AA) — regrouping announced without moving focus.
Three-level grouping as a tree Tree: the table groups by region, then status, then owner; each level has a header row with a count and subtotal, and data rows sit at level four. Three-level grouping as a treeInvoices (412 rows)Region: North (120)Status: Overdue (14)Owner: M. Ortiz (6)Owner: J. Lee (8)Status: Paid (106)Region: South (292)Status: Overdue (31)Status: Paid (261)
Each header row carries its level and the rows beneath it.

When to use — and when not to

Permalink to "When to use — and when not to"

Use multi-column grouping when users analyse data along two or three dimensions and need subtotals at each. Beyond three levels, screen reader users lose track of the path; consider a pivot table or a filter instead.

Do not group by high-cardinality columns such as invoice number or date-time; each group would hold one row. The misapplication: indentation alone for nesting, with flat role="row" elements that all report level 1.

Indentation only versus programmatic levels Comparison: indented rows with no levels sound flat to screen readers; treegrid rows with levels, set sizes and expanded state convey the hierarchy. Indentation only versus programmatic levels✗ Visual indentation onlyAll rows sound the sameParent group unknownNo expand stateDrag-only group bar✓ Treegrid levels"Level 2, Overdue, 14 rows"Path in header namearia-expanded per groupGroup-by menu with ordering

Annotated code example

Permalink to "Annotated code example"
<div class="group-by">
  <label for="g1">Group by</label>
  <select id="g1"><option>Region</option>…</select>
  <label for="g2">Then by</label>
  <select id="g2"><option>Status</option>…</select>
  <label for="g3">Then by</label>
  <select id="g3"><option value="">None</option><option>Owner</option>…</select>
</div>
<div role="treegrid" aria-label="Invoices grouped by region, status" aria-rowcount="412">
  <div role="row" aria-level="1" aria-expanded="true" aria-posinset="1" aria-setsize="2">
    <div role="gridcell" tabindex="0">Region: North — 120 invoices, 48,200.00</div>
  </div>
  <div role="row" aria-level="2" aria-expanded="false" aria-posinset="1" aria-setsize="2">
    <div role="gridcell" tabindex="-1">North › Status: Overdue — 14 invoices, 6,410.00</div>   <!-- path in the label -->
  </div>
</div>
<p role="status" class="sr-only" id="group-status"></p>
function onGroupingChange(cols) {
  regroup(cols);
  status.textContent = `Grouped by ${cols.join(', then ')}. ${topGroups.length} top-level groups.`;  // SC 4.1.3
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key On group row Announcement
Right Arrow Expand group “expanded, 14 rows”
Left Arrow Collapse, or move to parent “collapsed” / parent name
Down Arrow Next visible row Row content, “level 3”
Home / End First / last row Row content
Group-by select Change grouping “Grouped by Region, then Status”
Changing the grouping from the keyboard Sequence: the user changes the second grouping select to Owner; the grid regroups; a status message announces the new grouping; focus stays on the select. Changing the grouping from the keyboardUserSelectGridStatuschoose Owner as secondlevelregroup Region › Owner"Grouped by Region, thenOwner"announced politely
The control keeps focus; the grid changes behind it.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Offer select controls for grouping order. Three labelled selects (“Group by”, “Then by”, “Then by”) are simpler than a drag bar and fully keyboard accessible. If you keep a drag bar, add the selects as its alternative.
  2. Compute group rows with levels. Each group header row gets aria-level equal to its depth, aria-posinset and aria-setsize among siblings, and aria-expanded. Data rows get the next level down.
  3. Put the path and counts in the header label. “North › Overdue — 14 invoices, 6,410.00” tells a user exactly where they are without walking back up the tree.
  4. Keep aggregates in cells. If subtotals appear in numeric columns, put them in gridcells under the right column so column headers are announced with them.
  5. Announce regrouping. After a change, announce the new grouping and number of top-level groups, and keep focus on the control that changed.

Integration context

Permalink to "Integration context"

Single-level group headers are covered in accessible group header rows, collapse behaviour in collapsing row groups with aria-expanded and subtotals in announcing totals and summary rows. For cross-tabulation, see accessible pivot tables. Treegrid navigation is shared with tree views and hierarchical navigation.

Gotchas

Permalink to "Gotchas"

aria-level with native tables. aria-level is only supported on rows in a treegrid; in a plain table, put the level in the group header text (“Level 2: Overdue”).

Virtualization. With virtualized rows, aria-posinset and aria-setsize must be computed from data, not from rendered nodes.

Sorting inside groups. Sorting reorders rows within each group; say so in the sort announcement: “Sorted by amount within groups”.

Edge cases

Permalink to "Edge cases"
  • Empty groups after filtering. Remove them rather than showing “0 rows” headers that waste navigation.
  • Same value at different levels. If “Owner: M. Ortiz” appears under several regions, the path in the label distinguishes them.
  • Collapsed by default. Large data sets often start collapsed; say how many groups exist so users know where to begin.

Worked example

Permalink to "Worked example"

A finance team grouped invoices by region, then status, then owner in a commercial grid with a drag-to-group bar. Screen reader users heard a flat list of rows with no levels and could not change the grouping. The team added three grouping selects, switched the grid to treegrid mode with levels and set sizes, put the full path and subtotal in each group header, and announced regrouping. A blind analyst could now answer “how much is overdue in the North for each owner” in under a minute.

Common mistakes

Permalink to "Common mistakes"
  • Indentation without levels.
  • Drag-only grouping.
  • Group headers without counts or paths.
  • Moving focus into the grid after regrouping.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Expose grouping as a data-grid prop with an accessible group-by control included, and generate path labels from the grouping definition so teams cannot forget them.

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

With NVDA and Firefox, tab to the “Group by” selects and change the second level. Listen for the regrouping message and confirm that focus stays on the select. Move into the treegrid and arrow down through group rows: NVDA should report “level 1”, “level 2” and the expanded state, and each header should read its path and count. Press Left Arrow on a level 3 row to move to its parent and confirm the parent is announced. In JAWS, repeat with table navigation keys. In VoiceOver, where levels are announced less reliably, confirm that the path in each header label still gives full context.

FAQ

Permalink to "FAQ"
How many grouping levels are reasonable?

Two or three. Beyond that, use filters or a pivot table.

Should group headers be focusable?

Yes, in a treegrid; they carry expand state and summaries.

Can I keep a drag-to-group bar?

Yes, alongside selects or a menu that achieves the same result.

Where do subtotals go?

In group header rows or summary rows, in cells aligned under their columns.

How do screen readers announce level changes?

NVDA and JAWS announce “level 2” when moving between levels in a treegrid; VoiceOver support varies, so the path in the label is the fallback.

Does grouping need a live region?

Only for the regrouping action itself; expand and collapse are conveyed by aria-expanded.

Permalink to "Related"

← Back to Row Grouping & Totals in Data Tables