Skip to content
Niko Table

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).

Open in
Region
Order ID
Customer
Status
Date
Product
Amount
North America· 5 rows
$1,977.98
ORD-001John DoedeliveredJan 15, 2024Premium Widget$299.99
ORD-004Alice WilliamsdeliveredJan 22, 2024Starter Pack$79.99
ORD-007Ethan HuntpendingFeb 1, 2024Basic Kit$149.50
ORD-010Hannah LeedeliveredFeb 12, 2024Enterprise Suite$1,299.00
ORD-012Julia RossdeliveredMar 1, 2024Basic Kit$149.50
Europe· 4 rows
$2,347.49
ORD-002Jane SmithshippedJan 18, 2024Basic Kit$149.50
ORD-005Charlie BrownshippedJan 25, 2024Enterprise Suite$1,299.00
ORD-008Fiona GreendeliveredFeb 5, 2024Pro Bundle$599.00
Preview with Controlled State
Open in
Region
Order ID
Customer
Status
Date
Amount
North America(4)
undefined$1,828.48
ORD-001John DoedeliveredJan 15, 2024$299.99
ORD-004Alice WilliamsdeliveredJan 22, 2024$79.99
ORD-007Ethan HuntpendingFeb 1, 2024$149.50
ORD-010Hannah LeedeliveredFeb 12, 2024$1,299.00
Europe(3)
undefined$2,047.50
ORD-002Jane SmithshippedJan 18, 2024$149.50
ORD-005Charlie BrownshippedJan 25, 2024$1,299.00
ORD-008Fiona GreendeliveredFeb 5, 2024$599.00
Asia Pacific(3)
undefined$978.98
Current Table State
Live view of the current table state for demonstration purposes
Grouped By:region
Expanded Groups:All
Total Items:10
Sorting:None
Page:1 (Size: 10)
Hidden Columns:0
View Full State Object
Grouping:
[
  "region"
]
Expanded:
true
Sorting:
[]

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.

pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-column-group @niko-table/data-table-column-sort @niko-table/data-table-pagination @niko-table/data-table-search-filter @niko-table/data-table-view-menu

First time using @niko-table? See the Installation Guide to set up the registry.

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.

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. Return false and that band falls back to the built-in row.
  • DataTableGroupedRowCells is exported, so a custom row can render the built-in cells for the cases it does not special-case.

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.).

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.

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>
),
}

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")
},
}
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 }}.

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

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.

  • Group bands render in DataTableBody and DataTableVirtualizedBody — the same DataTableGroupedRows marker 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 fixedRowHeight off. 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 / manualGrouping is out of scope for this release (TanStack notes it needs heavy custom rendering).

✅ 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 — manualGrouping is out of scope for this release