React table row grouping — single-level groupBy, per-group aggregates
▶ Try it live: open a Mantine starter in StackBlitz — this page’s feature is already wired in src/App.tsx (groupBy="role" + groupAggregates); edit it in the browser, no install. Other UI kits →
▶ See it working: collapse groups and read per-group subtotals in the live demo — a real table you can click, not a recording.
Group rows by one column with groupBy and optional per-group subtotals via
groupAggregates — the same mapper signature as summaryRow. Omit
groupBy and the table never inserts group header rows (package DNA: opt-in).
Example
Section titled “Example”import { DataTable } from "@adapttable/mantine"; // or mui, chakra, antd, radix, base-ui, shadcn, unstyled
interface Person { id: string; name: string; team: string; budget: number;}
const PEOPLE: Person[] = [ { id: "1", name: "Aisha", team: "Core", budget: 42_000 }, { id: "2", name: "Jonas", team: "Platform", budget: 38_000 }, { id: "3", name: "Mei", team: "Core", budget: 51_000 },];
export function People() { return ( <DataTable data={PEOPLE} columns={[ { key: "name", sortable: true }, { key: "team", sortable: true }, { key: "budget", accessor: (r) => `$${r.budget.toLocaleString()}`, sortValue: (r) => r.budget, }, ]} rowKey={(r) => r.id} groupBy="team" groupAggregates={(rows) => ({ budget: ( <b>${rows.reduce((sum, r) => sum + r.budget, 0).toLocaleString()}</b> ), })} /> );}How it works
Section titled “How it works”- Opt-in. Pass
groupBy(a column key) or setsource.groupByviauseFrontendData/ URL state — without it, grouping stays fully dormant. - Single level. One grouping column at a time (no nested groups, no drag-to-group panel).
- Frontend tier only. Grouping needs the full filtered row set in memory
(
allFilteredRows). Server-paginated sources log a dev-mode warning and ignore grouping — see Data tiers. - Shared mapper.
groupAggregates(rows)uses the same(rows) => Partial<Record<string, ReactNode>>shape assummaryRow; reuse one function for both if the math is identical. - Expand / collapse. Groups start expanded. Collapse state is ephemeral
(not URL-synced).
groupByitself serializes to the URL like sort and filters. - Selection. When row checkboxes are enabled, each group header exposes a tri-state checkbox over its leaf rows.
Options
Section titled “Options”| Prop / field | Type | Default | Description |
|---|---|---|---|
groupBy | string | null | — | Column key to group by; its presence arms grouping (still requires a frontend data source). |
onGroupByChange | (groupBy: string | null) => void | — | Controlled change channel; falls back to source.setGroupBy. |
groupAggregates | (rows: readonly TRow[]) => Partial<Record<string, ReactNode>> | — | Per-group cells — same signature as summaryRow. Omit for headers without subtotals. |
collapsedGroupIds | readonly string[] | — | Controlled collapsed group keys (ephemeral — not URL-synced). |
onCollapsedGroupIdsChange | (ids: string[]) => void | — | Controlled collapse channel; uncontrolled mode uses internal state. |
labels | TableLabels | English | Override expandGroup, collapseGroup, and groupCount for header controls. |
- Bucketing uses the column’s
sortValuewhen present, otherwise a path lookup on the column key — never the JSXaccessor. - Works on desktop rows and mobile cards, LTR and RTL, with and without
virtualize(virtual windows count collapsed groups as one row). - Out of scope (by design): multi-level nesting, pivot mode, drag-to-group, and Excel-style aggregation pickers.
- Ant Design maps group headers onto its high-level
Tablevia custom row rendering; every other kit renders native group header rows/cards.
See it live in the demo — rows are grouped by team with a budget subtotal per group.