Grouping Table
Group rows by column values with expand/collapse and TanStack aggregations (sum, count, and custom getGroupingValue for month buckets).
Collapse orders into region, status, or calendar-month bands from the column actions menu — the same Group by / Stop grouping by pattern used by MUI X and other data grids. Nested groups follow the order you group columns; numeric columns can roll up with TanStack aggregationFn (this demo sums Amount).
Preview with Controlled State
Introduction
Section titled “Introduction”Grouping collapses rows that share a value into a single expandable band, with an optional rollup (sum, count, …) on the group row. Add Group by to any column’s actions menu and users pick the grouping at runtime; nest bands by grouping more than one column. Built on TanStack Table’s getGroupedRowModel, so you drop in one registry piece and mount the option where you want it.
Installation
Section titled “Installation”First time using
@niko-table? See the Installation Guide to set up the registry.
Group rows
Section titled “Group rows”Nest DataTableGroupedRows in the body. That single line is the whole opt-in — it renders the group bands and is what feature detection reads to turn enableGrouping and enableExpanding on, so there is no config flag to set and no renderer to wire up.
import { DataTableGroupedRows } from "@/components/niko-table/components/data-table-grouped-rows"
<DataTable> <DataTableHeader /> <DataTableBody> <DataTableGroupedRows /> </DataTableBody></DataTable>Leave it out and nothing about the table changes — a grouped row model with no DataTableGroupedRows renders no bands.
The built-in band shows a chevron, the group value and a · N rows count, plus one aggregated cell per column that defines aggregatedCell / aggregationFn.
Your own group row
Section titled “Your own group row”Nest a component instead. It reads the group from useDataTableGroupRow() — no render prop, no callback:
import { DataTableGroupedRows, useDataTableGroupRow,} from "@/components/niko-table/components/data-table-grouped-rows"
function RegionGroupRow() { const { groupLabel, count, isExpanded, toggle } = useDataTableGroupRow()
return ( <TableCell colSpan={4} onClick={toggle}> {groupLabel} — {count} orders {isExpanded ? "▾" : "▸"} </TableCell> )}
<DataTableBody> <DataTableGroupedRows> <RegionGroupRow /> </DataTableGroupedRows></DataTableBody>Your component renders the row’s cells; the body keeps the surrounding <TableRow> so column widths, parity and data attributes still line up with the leaf rows beneath.
Two escape hatches:
enabledFor— a per-group predicate. Returnfalseand that band falls back to the built-in row.DataTableGroupedRowCellsis exported, so a custom row can render the built-in cells for the cases it does not special-case.
Column menu: Group by
Section titled “Column menu: Group by”Compose group actions into the header menu like other column controls:
import { DataTableColumnGroupOptions } from "@/components/niko-table/components/data-table-column-group"
header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Region" /> <DataTableColumnActions> <DataTableColumnSortOptions /> <DataTableColumnGroupOptions /> </DataTableColumnActions> </DataTableColumnHeader>),Opt columns out with enableGrouping: false so Group by does not appear (IDs, free-text fields, etc.).
Expand / collapse all
Section titled “Expand / collapse all”The group toggle lives on each band, but a toolbar shortcut is handy. table.toggleAllRowsExpanded() reads from context — no extra state:
import { useDataTable } from "@/components/niko-table/core/data-table-context"
function GroupingExpansionControls() { const { table } = useDataTable<Order>() const hasGrouping = table.state.grouping.length > 0
return ( <> <Button variant="outline" size="sm" disabled={!hasGrouping} onClick={() => table.toggleAllRowsExpanded(true)} > Expand All </Button> <Button variant="outline" size="sm" disabled={!hasGrouping} onClick={() => table.toggleAllRowsExpanded(false)} > Collapse All </Button> </> )}Drop <GroupingExpansionControls /> inside DataTableToolbarSection next to the search input.
Aggregations
Section titled “Aggregations”Set aggregationFn on columns that should roll up on group rows. Built-ins include sum, count, min, max, mean, median, and more — see TanStack Grouping.
{ accessorKey: "amount", aggregationFn: "sum", cell: ({ getValue }) => formatCurrency(Number(getValue())), aggregatedCell: ({ getValue }) => ( <span className="font-medium tabular-nums"> {formatCurrency(Number(getValue()))} </span> ),}Group by month
Section titled “Group by month”Use getGroupingValue to derive a bucket (e.g. YYYY-MM) without changing the underlying date cell for leaf rows:
{ accessorKey: "date", getGroupingValue: row => row.date.slice(0, 7), cell: ({ getValue, row }) => { const value = String(getValue()) if (row.getIsGrouped()) return formatMonthKey(value) return new Date(`${value}T00:00:00`).toLocaleDateString("en-US") },}Controlled state
Section titled “Controlled state”const [grouping, setGrouping] = useState<GroupingState>(["region"])const [expanded, setExpanded] = useState<ExpandedState>(true)
<DataTableRoot data={data} columns={columns} state={{ grouping, expanded }} onGroupingChange={setGrouping} onExpandedChange={setExpanded}>Or seed once with initialState={{ grouping: ["region"], expanded: true }}.
Config
Section titled “Config”| Option | Purpose |
|---|---|
config.enableGrouping |
Force grouping on/off. Usually unnecessary — composing DataTableGroupedRows or a column group menu turns it on |
config.groupedColumnMode |
'reorder' (default), 'remove', or false — TanStack placement of grouped columns |
config.enableExpanding |
Forced on when grouping is active so group rows can collapse |
Tree vs Grouping
Section titled “Tree vs Grouping”Both expand/collapse, but they are different features — use one per table:
| Tree Table | Grouping | |
|---|---|---|
| Source | Nested data via getSubRows |
Flat data + column buckets |
| Who defines structure | Your data model | User / grouping state |
| Aggregations | Not the main model | aggregationFn / aggregatedCell |
| Typical UI | Indent + chevron per level | Group by in the column menu + (n) count |
Row Expansion is a third pattern: one flat row opens an inline detail panel — not nested rows and not column buckets.
Constraints
Section titled “Constraints”- Group bands render in
DataTableBodyandDataTableVirtualizedBody— the sameDataTableGroupedRowsmarker in either. The DnD bodies do not render bands: manual row order and grouped order conflict, so grouping there is out of scope by design. - In a virtualized body, leave
fixedRowHeightoff. A band is taller than a leaf row, so it has to be measured rather than assumed; with fixed heights on, the rows below it drift. - Prefer not combining row DnD with grouping (manual order vs grouped order conflict) — same guidance as sorting + row DnD.
- Column DnD, sorting, filtering and pagination remain compatible.
- Server-side /
manualGroupingis out of scope for this release (TanStack notes it needs heavy custom rendering).
When to use
Section titled “When to use”✅ Use grouping when:
- Summarizing large lists by region, status, category, or month
- Showing rollups (sum of amount, count of rows) on group headers
- Migrating from MUI X / similar grids that expose Group by in the column menu
❌ Consider other options when:
- Rows form a fixed parent/child hierarchy — use a Tree Table instead
- You need per-row detail panels — use Row Expansion
- Grouping is computed on the server —
manualGroupingis out of scope for this release
Next steps
Section titled “Next steps”- Tree Table — nested data with
getSubRows - Row Expansion Table — inline detail panels
- Column Pinning Table — keep ID columns visible while scrolling grouped views
- Faceted Filter Table — narrow before grouping