Rowpine for TanStack Table
Buy a license

TanStack Table pivot table

TanStack Table groups rows but has no pivot. Build one from code, let users design it with a field list and drop wells, and export it to Excel.

Updated November 15, 20266 min readTested with rowpine 1.0.3, TanStack Table 9.3.1 and 8.21.3, React 19.3.1
Edit on GitHub

TanStack Table can group rows: regions, then countries under them, with a total on each group row. A pivot table goes one step further and spreads a field across the columns too. Revenue by region down the side and by quarter across the top is a pivot; TanStack has no feature for it.

Building one yourself means reading the distinct values of the column fields, generating a column definition for each combination of value and measure, aggregating every cell, and adding subtotal rows, a total column and a grand total. This guide does it with Rowpine instead, in three steps:

  1. a pivot from code, rendered with a shadcn/ui table;
  2. a designer where users drag fields into rows, columns and values themselves;
  3. an Excel export of whatever pivot is on screen.
A pivot of revenue with regions and countries down the side and quarters across
Revenue by region and country down the side, quarters across, with subtotals, a Total column and a grand total.

Grouping or pivot?#

TanStack grouping Pivot
Rows Grouped by one or more fields Grouped by one or more fields
Columns The columns you defined Generated from the values of the column fields
Cells Your row data, totals on group rows Aggregated values at each row and column intersection
Totals Per group row Per row group, per column, and a grand total

If your users ask "how much per region, per quarter", they want a pivot.

Install#

The rowpine package has the pivot engine. The pivot registry item installs two components into your project: pivot-table.tsx and pivot-designer.tsx. Add the export menu too for the last step.

Terminal
npm install rowpine @tanstack/react-table
npx shadcn add https://rowpine.com/r/pivot.json
npx shadcn add https://rowpine.com/r/export-menu.json

The examples use orders with a region, country, customer, product, quarter, quantity and amount. Any array of objects works; the Excel guide shows the data.

A pivot from code#

A pivot is a config: fields down the side (rows), fields across the top (cols), and the measures at each intersection (values). usePivot builds it, PivotTable renders it:

sales-pivot.tsx
import { pivotRowStyle, type PivotConfig } from "rowpine"

import { ExportMenu } from "@/components/export-menu"
import { PivotTable, usePivot } from "@/components/pivot-table"
import { orders, type Order } from "./orders"

const usd = (v: unknown) =>
  typeof v === "number" ? `$${Math.round(v).toLocaleString("en-US")}` : ""

// Module scope keeps the config stable; usePivot recomputes when it changes.
const config: PivotConfig<Order> = {
  rows: ["region", "country"],
  cols: ["quarter"],
  values: [{ field: "amount", agg: "sum", label: "Revenue", format: usd }],
  rowHeader: "Region / Country",
}

export function SalesPivot() {
  const pivot = usePivot(orders, config, { numFmt: '"$"#,##0' })

  return (
    <div className="grid gap-3">
      <div className="flex justify-end">
        <ExportMenu table={pivot.table} fileName="revenue" pdfTitle="Revenue" rowStyle={pivotRowStyle} />
      </div>
      <PivotTable pivot={pivot} />
    </div>
  )
}

What each part does:

Key What it does
rows Fields down the side, outermost first. Each one is a level you can expand and collapse.
cols Fields across the top, outermost first. Each distinct value becomes a column group.
values The measures: field, agg, label and an optional format. Several measures sit side by side under each column.
rowHeader The header of the first column. Default: the row fields joined with /.
rowSubtotals Group rows carry subtotals. Default true.
grandTotalRow, grandTotalCol The grand total row and the Total column. Default true.

agg is one of sum, avg, min, max, count, countDistinct, first or last, or your own function from the values under a cell to the number to show. The sales commissions demo uses a custom one for a tiered commission.

Keep the config stable, at module scope or in useMemo. usePivot recomputes when the config changes, which is what you want when the user changes it and wasted work on every render otherwise.

The third argument, { numFmt: '"$"#,##0' }, is the Excel number format for the value columns. It only affects exports.

Let users build the pivot#

A fixed pivot answers one question. The designer lets users ask their own: a field list on the left, four wells (Filters, Columns, Rows, Values), and the pivot under them.

sales-designer.tsx
import { pivotRowStyle, type PivotField, type PivotPreset } from "rowpine"

import { ExportMenu } from "@/components/export-menu"
import { PivotDesigner } from "@/components/pivot-designer"
import { orders, type Order } from "./orders"

const usd = (v: unknown) =>
  typeof v === "number" ? `$${Math.round(v).toLocaleString("en-US")}` : ""

const fields: PivotField<Order>[] = [
  { field: "region", label: "Region", kind: "dimension", group: "Where" },
  { field: "country", label: "Country", kind: "dimension", group: "Where" },
  { field: "customer", label: "Customer", kind: "dimension", group: "Who" },
  { field: "product", label: "Product", kind: "dimension", group: "What" },
  { field: "quarter", label: "Quarter", kind: "dimension", group: "When" },
  { field: "amount", label: "Revenue", kind: "measure", group: "Numbers", format: usd },
  { field: "quantity", label: "Units", kind: "measure", group: "Numbers" },
]

const presets: PivotPreset[] = [
  {
    name: "Revenue by region and quarter",
    layout: {
      rows: ["region", "country"],
      cols: ["quarter"],
      values: [{ field: "amount", agg: "sum", label: "Revenue" }],
      filters: [],
    },
  },
  {
    name: "Units by product",
    layout: {
      rows: ["product"],
      cols: ["region"],
      values: [{ field: "quantity", agg: "sum", label: "Units" }],
      filters: [],
    },
  },
]

export function SalesDesigner() {
  return (
    <PivotDesigner
      data={orders}
      fields={fields}
      presets={presets}
      pivotOptions={{ numFmt: '"$"#,##0' }}
      actions={(pivot) => (
        <ExportMenu table={pivot.table} fileName="sales" rowStyle={pivotRowStyle} />
      )}
    />
  )
}
The pivot designer with a field list grouped under Where, Who, What, When and Numbers, and Region in Rows and Revenue in Values
The designer. Users tick a field or drag it into a well; the pivot updates as they go.

A field's kind decides where it can go: a dimension goes to Rows, Columns or Filters, a measure to Values or Filters. group sorts the field list under headings. On a value chip, users pick the aggregation from a menu.

presets fill the Presets menu with layouts you wrote. A layout is plain JSON (rows, cols, values, filters), so you can also save the user's layout with onLayoutChange and restore it later with layout: in local storage, in your database, or in the URL.

Export the pivot to Excel#

Both components give you pivot.table, a table view of the pivot for the exporters. Pass it to the export menu with pivotRowStyle, which makes the group, subtotal and grand total rows bold:

tsx
<ExportMenu table={pivot.table} fileName="revenue" rowStyle={pivotRowStyle} />
The exported pivot in Excel with Q1 / Revenue to Total / Revenue columns and countries as an outline level
The exported pivot. Each column header joins its column group and measure, and the countries are an outline level under their region.

In the file:

  • The first column header is your rowHeader, and each value column joins its column group with the measure: Q1 / Revenue. One header row keeps the sheet easy to filter and sort.
  • Row groups become Excel outline levels.
  • The cells are numbers with the numFmt you passed, so they can be summed and charted.
  • Every row is in the file, including groups collapsed on screen, and only the rows the designer's filters let through.

exportXlsx(pivot.table, { rowStyle: pivotRowStyle }) does the same from your own button, and PDF and CSV work the same way.

Render it with your own TanStack table#

PivotTable is a regular shadcn/ui table. If you would rather use the table component you already have, createPivot returns everything TanStack needs: data (rows nested through subRows), columns (column definitions with header groups for the column fields) and table (for exports).

use-revenue-pivot-table.ts
import * as React from "react"
import {
  createExpandedRowModel,
  rowExpandingFeature,
  tableFeatures,
  useTable,
  type ColumnDef,
} from "@tanstack/react-table"
import { createPivot, type PivotTreeRow } from "rowpine"

import { orders } from "./orders"

const features = tableFeatures({
  rowExpandingFeature,
  expandedRowModel: createExpandedRowModel(),
})

export function useRevenuePivotTable() {
  // rows nested through subRows, plus TanStack column definitions
  const pivot = React.useMemo(
    () =>
      createPivot(orders, {
        rows: ["region", "country"],
        cols: ["quarter"],
        values: [{ field: "amount", agg: "sum", label: "Revenue" }],
      }),
    []
  )

  return useTable({
    features,
    data: pivot.data,
    // Plain column objects (id, header, accessorFn, columns); give them v9's type.
    columns: pivot.columns as unknown as ColumnDef<typeof features, PivotTreeRow>[],
    getSubRows: (row) => row.subRows,
    initialState: { expanded: true },
  })
}

Render the result as any tree table: row.depth for the indent, row.getToggleExpandedHandler() on the first cell. The value columns come with a cell that formats each number, so <table.FlexRender cell={cell} /> shows 4,701.35, not 4701.349999999999; give the measure a format for currency (rowpine 1.0.3 and later). Register more features, such as sorting, the same way as for any TanStack table. The pivot in your own table example has the full component.

License#

CSV export is free. Pivot tables, the designer, and Excel and PDF export run without a key, with one notice line in exported files; a license removes it and is a one-time payment per developer with a year of updates. Try the designer with the guided tour on the home page, or open the sales pivot demo.

Try it in your project

Everything runs without a license key; Excel and PDF files carry one notice line until you add one.

More guides