Summary Rows
Summaries are post-data rows that appear below a table. They are report-mode only — for excel-table mode, use the totals row feature instead.
Every summary is defined as a reducer: init, step, and finalize. This means the exact same schema works in both buffered and streaming mode — step() is called once per row, with no rows held in memory.
Styling support depends on the summary kind:
- reducer summaries support static
style, dynamicstyle(value), andconditionalStyle - formula summaries support static
styleandconditionalStyle
For the styling models behind those fields, see Cell Styles and Conditional Styles.
Summary cell
summary.cell() reduces row data into a computed value:
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{
invoiceId: string;
amount: number;
taxRate: number;
}>()
.column("invoiceId", { header: "Invoice #", accessor: "invoiceId" })
.column("amount", {
header: "Amount",
accessor: "amount",
style: { numFmt: "$#,##0.00" },
summary: (summary) => [
summary.cell({
init: () => 0,
step: (acc, row) => acc + row.amount,
finalize: (acc) => acc,
style: { numFmt: "$#,##0.00", font: { bold: true } },
}),
],
})
.column("tax", {
header: "Tax",
accessor: (row) => row.amount * row.taxRate,
style: { numFmt: "$#,##0.00" },
summary: (summary) => [
summary.cell({
init: () => 0,
step: (acc, row) => acc + row.amount * row.taxRate,
finalize: (acc) => acc,
style: { numFmt: "$#,##0.00", font: { bold: true } },
}),
],
})
.build();
Formula summaries
summary.formula() declares an Excel range formula instead of a JavaScript reduction. Use this when you want the summary cell to recalculate in Excel if the underlying data changes.
The column.cells() context gives you .sum(), .average(), .count(), .min(), .max() — each emits the corresponding range formula (e.g. =SUM(C2:C1001)):
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{ amount: number; qty: number }>()
.column("qty", {
header: "Qty",
accessor: "qty",
summary: (summary) => [summary.formula(({ column }) => column.cells().sum())],
})
.column("amount", {
header: "Amount",
accessor: "amount",
style: { numFmt: "$#,##0.00" },
summary: (summary) => [
// fx is available for wrapping the range formula in another function
summary.formula(({ column, fx }) => fx.round(column.cells().sum(), 2)),
],
})
.build();
Shorthand strings
For the common case, pass a shorthand string instead of a callback:
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{ revenue: number; units: number; price: number }>()
.column("revenue", {
accessor: "revenue",
summary: (summary) => [summary.formula("sum")],
})
.column("units", {
accessor: "units",
summary: (summary) => [summary.formula("count")],
})
.column("price", {
accessor: "price",
summary: (summary) => [summary.formula("average")],
})
.build();
Available shorthands: "sum", "average", "count", "min", "max".
Summary styling
Reducer summaries can compute style from the finalized summary value:
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{ amount: number }>()
.column("amount", {
accessor: "amount",
summary: (summary) => [
summary.cell({
init: () => 0,
step: (acc: number, row) => acc + row.amount,
finalize: (acc: number) => acc,
style: (value) => ({
numFmt: "$#,##0.00",
font: {
bold: true,
color: { rgb: (value as number) >= 0 ? "166534" : "991B1B" },
},
}),
}),
],
})
.build();
Both reducer and formula summaries can also use conditionalStyle for Excel-native live styling:
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{ amount: number }>()
.column("amount", {
accessor: "amount",
summary: (summary) => [
summary.formula("sum", {
style: {
numFmt: "$#,##0.00",
font: { bold: true },
},
conditionalStyle: (conditional) =>
conditional.when(({ cell }) => cell.current().lt(0), {
fill: { color: { rgb: "FEE2E2" } },
font: { color: { rgb: "991B1B" }, bold: true },
}),
}),
],
})
.build();
Labels and alignment
summary.label() places static text in a summary cell. Use it to label what the row represents.
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{
category: string;
units: number;
revenue: number;
}>()
.column("category", {
accessor: "category",
summary: (summary) => [
summary.label("TOTAL", {
style: { font: { bold: true }, alignment: { horizontal: "right" } },
}),
],
})
.column("units", {
accessor: "units",
summary: (summary) => [summary.formula("sum")],
})
.column("revenue", {
accessor: "revenue",
style: { numFmt: "$#,##0.00" },
summary: (summary) => [summary.formula(({ column, fx }) => fx.round(column.cells().sum(), 2))],
})
.build();
empty() vs spacer()
These two helpers are easy to confuse, so it helps to treat them separately:
| Helper | Value | Default summary styling | Typical use |
|---|---|---|---|
summary.empty() | empty | yes | keep a normal summary row shape with no value in this column |
summary.spacer() | empty | no | intentionally leave a visual gap without default summary emphasis |
Use spacer() when you want a blank cell that does not inherit the default summary emphasis:
import { createExcelSchema } from "typed-xlsx";
const schema = createExcelSchema<{
orderId: string;
sku: string;
qty: number;
amount: number;
}>()
.column("orderId", {
header: "Order ID",
accessor: "orderId",
summary: (summary) => [summary.label("TOTAL")],
})
.column("sku", {
accessor: "sku",
// Spacer: blank cell without default summary styling
summary: (summary) => [summary.spacer()],
})
.column("qty", {
accessor: "qty",
summary: (summary) => [summary.formula("sum")],
})
.column("amount", {
accessor: "amount",
style: { numFmt: "$#,##0.00" },
summary: (summary) => [summary.formula("sum")],
})
.build();
Multiple summary rows
Return multiple definitions from the callback to produce multiple summary rows. Each array index maps to the same summary row index across all columns — index 0 is the first summary row, index 1 is the second, and so on.
A practical example: a P&L report with a pre-tax total row and a post-tax total row:
import { createExcelSchema } from "typed-xlsx";
const plSchema = createExcelSchema<{
lineItem: string;
amount: number;
}>()
.column("lineItem", {
header: "Line Item",
accessor: "lineItem",
summary: (summary) => [
summary.label("NET REVENUE", { style: { font: { bold: true } } }),
summary.label("NET REVENUE + TAX", { style: { font: { bold: true } } }),
],
})
.column("amount", {
header: "Amount",
accessor: "amount",
style: { numFmt: "$#,##0.00" },
summary: (summary) => [
summary.cell({
init: () => 0,
step: (acc, row) => acc + row.amount,
finalize: (total) => total,
style: { numFmt: "$#,##0.00", font: { bold: true } },
}),
summary.cell({
init: () => 0,
step: (acc, row) => acc + row.amount,
finalize: (total) => total * 1.2, // +20% VAT
style: { numFmt: "$#,##0.00", font: { bold: true } },
}),
],
})
.build();
Summary availability
Summaries are defined at the column level in the schema, but they only exist if:
- The schema mode is
"report"(not"excel-table") - The column is selected into the final table