Skip to content

Grouping and aggregation

Group rows by a column and each group becomes an expandable row with a count and, in every other column, an aggregate of its members: a sum of amounts, a range of dates, a count of distinct values. Each column kind brings a sensible aggregation; a footer can show the grand total.

Grouped byTeam
842 distinct$5,059.006430.5%May 2, 2023 – Sep 8, 2026
Meera SharmaFreeActive$0.0010.5%Jun 26, 2024
Sofia RossiEnterpriseActive$1,659.002161.3%Mar 26, 2026
Zara SharmaFreeInvited$0.00159%Sep 8, 2026
Vihaan PrabhakaranFreeInvited$0.0013.4%Dec 8, 2024
Meera KimFreeActive$0.00124.1%Oct 23, 2025
Ishaan DuboisTeamActive$1,856.006463%Apr 6, 2026
Lucas BoseTeamInvited$1,508.005212%Dec 13, 2025
Freya OkaforProActive$36.00321%May 2, 2023
$53,633max 64
0 of 8 row(s) selected.
Page 1 of 1

Three ways in, all equivalent:

  • The Group button in the toolbar lists every groupable column with a checkbox, plus Clear grouping once something is grouped.
  • A column’s menu has Group by Team (or Ungroup when it already is).
  • The grouping bar on the left of the toolbar shows the grouped columns as chips, in order; the × on a chip ungroups that column.

To start grouped, set initialState.grouping. Group rows have the id columnId:value (nested groups join with >), so you can open one from initialState.expanded too:

<DataGrid
data={data}
columns={columns}
initialState={{ grouping: ["team"], expanded: { "team:Platform": true } }}
/>

Every column kind has a default:

Kind Default aggregationFn Shown in the group row as
number, currency sum The total, formatted like the column.
percent mean The average, as a percentage.
date, datetime extent The earliest and latest, 12 Jan 2024 – 3 Mar 2026.
boolean count How many rows.
enum, text, link, tags uniqueCount How many distinct values.
custom, col.display none Nothing, until you set one.

Pick another by name. The names the grid registers are sum, min, max, extent, mean, median, unique, uniqueCount, count, first and last, plus "auto" (sum for numbers, extent for dates):

col.number("seats", { header: "Seats", aggregationFn: "max" })

Or write your own. An aggregation is an object with an aggregate function that receives the group’s rows and a getValue for the column; constructAggregationFn only types it:

import { constructAggregationFn, type DataGridFeatures } from "@/components/ui/data-grid"
const weightedUsage = constructAggregationFn<DataGridFeatures, Person, number, number>({
aggregate: ({ rows, getValue }) => {
const seats = rows.reduce((n, r) => n + r.original.seats, 0)
return seats ? rows.reduce((n, r) => n + getValue(r) * r.original.seats, 0) / seats : 0
},
})
col.percent("usage", { header: "Usage", aggregationFn: weightedUsage })

The default aggregatedCell formats the value the way the column formats its cells (currency as currency, dates as dates), renders an extent as min – max, a count as a plain number and a unique list as the values or N values, and names the aggregation in the cell’s tooltip. Give the column its own aggregatedCell to render it differently; getValue() is the aggregate:

col.enum("status", {
header: "Status",
options: STATUS_OPTIONS,
aggregatedCell: ({ getValue }) => <span className="text-muted-foreground">{String(getValue())} distinct</span>,
})

A column with a footer gets a footer row, pinned to the bottom of the grid. column.getAggregationValue() runs the column’s aggregation over every row that passes the current filters, on every page, grouped or not, so it is the grand total rather than the page total:

col.currency("mrr", {
header: "MRR",
currency: "USD",
footer: ({ column }) => usd.format(column.getAggregationValue<number>() ?? 0),
})

The result’s type is whatever the aggregation returns (number for sum, a [min, max] pair for extent), so pass it as the generic. The footer renders only when some column defines one.

  • Group rows. The grouped column’s cell shows a chevron, the formatted group value and the number of leaf rows in parentheses. Click it, or press Enter on a focused cell of the row, to open or close the group. Group rows are tinted and bold and carry data-grouped.
  • Several columns nest: grouping: ["team", "plan"] groups by team, then by plan inside each team. The chips show the order; the Group menu toggles membership.
  • Grouped columns move to the front (groupedColumnMode: "reorder"), in grouping order, and lose their drag handle while grouped. Pass table={{ groupedColumnMode: false }} to leave them where they are, or "remove" to hide them (the value is in the group row anyway).
  • Sorting runs after grouping: group rows are ordered by the sorted column’s value (the group value, or the aggregate for other columns) and the rows inside each group are sorted too.
  • Filtering runs before grouping, so counts and aggregates describe the filtered rows.
  • Paging counts group rows; the members of an open group render on the same page.
  • Selection. Ticking a group row selects every row in it; some selected shows indeterminate.
  • Editing is off on group rows.
  • Export skips group rows and writes the leaf rows in their grouped, sorted order.
  • Tags columns are not groupable by default: a row has several tags, and a group per array would be meaningless.
  • Persistence. grouping is part of the view the grid remembers and of saved views. URL state can own it too.
  • Server-side. server={{ grouping: true }} stops local grouping; you deliver the group rows. Aggregates computed by getAggregationValue() still run over the rows the grid has, which is the current page. See Server-side data.

Column options:

Option Type Default What it does
aggregationFn name, definition or array by kind How the column aggregates in a group row and in getAggregationValue(). Defaults are in the table above.
aggregatedCell render function built in Renders the aggregate in a group row; ctx.getValue() is the aggregate.
footer string or render function Adds the footer row; use ctx.column.getAggregationValue() for a total.
groupable boolean true false keeps the column out of the Group menu and its header menu. tags columns default to false.
enableGrouping boolean true TanStack’s switch; groupable: false is the one the menus read, so prefer it.
getGroupingValue (row) => unknown the cell value Group by something derived, such as the month of a date.
maxAggregationDepth number 0 For tree data: 1 aggregates over direct children instead of root rows.

Grid options:

Option Type Default What it does
grouping boolean true false removes the Group menu, the chips and the header items.
initialState.grouping string[] [] Column ids to group by, in order.
table.groupedColumnMode false | "reorder" | "remove" "reorder" Where grouped columns go.
server.grouping boolean false true makes grouping the server’s job.