Read, edit, and write Excel files in TypeScript
Open any .xlsx or start from an empty workbook. Change cells, styles, formulas, and charts through typed functions, then save a file that validates against the ECMA-376 schemas. It runs in Node 22 and later, and in the browser.
A worksheet and the code that built it
export function buildHeroWorkbook(): Workbook {
const wb = createWorkbook();
const ws = addWorksheet(wb, 'Q3 sales');
const white = makeColor({ rgb: 'FFFFFFFF' });
const green = makeColor({ rgb: 'FF168A4F' });
const header = registerCellStyle(wb, {
font: makeFont({ bold: true, color: white }),
fill: makePatternFill({ patternType: 'solid', fgColor: green }),
});
appendRow(ws, ['Region', 'Units', 'Revenue', 'Growth'], {
styleIds: [header, header, header, header],
});
setFreezePanes(ws, 'A2');
const regions = [
['North', 1_240, 186_000, 0.124],
['South', 980, 139_650, 0.081],
['East', 1_515, 242_400, 0.193],
['West', 760, 102_600, -0.036],
] as const;
const units = registerCellStyle(wb, { numberFormat: '#,##0' });
const money = registerCellStyle(wb, { numberFormat: '"$"#,##0' });
const percent = registerCellStyle(wb, { numberFormat: '0.0%' });
appendRows(ws, regions, { styleIds: [undefined, units, money, percent] });
// The library never evaluates formulas, so cache the sums it can work out:
// viewers that do not calculate then show a number instead of a blank.
const sum = (col: 1 | 2): number => regions.reduce((n, region) => n + region[col], 0);
const total = {
font: makeFont({ bold: true }),
border: makeBorder({ top: makeSide({ style: 'thin' }) }),
};
appendRow(
ws,
[
'Total',
makeFormula('SUM(B2:B5)', { cachedValue: sum(1) }),
makeFormula('SUM(C2:C5)', { cachedValue: sum(2) }),
],
{
styleIds: [
registerCellStyle(wb, total),
registerCellStyle(wb, { ...total, numberFormat: '#,##0' }),
registerCellStyle(wb, { ...total, numberFormat: '"$"#,##0' }),
registerCellStyle(wb, total),
],
},
);
setColumnWidths(ws, [14, 10, 14, 10]);
return wb;
}
| A | B | C | D | E | F | |
|---|---|---|---|---|---|---|
| 1 | Region | Units | Revenue | Growth | ||
| 2 | North | 1,240 | $186,000 | 12.4% | ||
| 3 | South | 980 | $139,650 | 8.1% | ||
| 4 | East | 1,515 | $242,400 | 19.3% | ||
| 5 | West | 760 | $102,600 | -3.6% | ||
| 6 | Total | 4,495 | $670,650 | |||
| 7 | ||||||
| 8 | ||||||
| 9 | ||||||
| 10 | ||||||
| 11 |
That sheet is real output. The code shown with it built the workbook, the library saved it and
read the bytes back, and getCellDisplayText put each value through its number format.
Download it and open it in Excel: the totals are live formulas. Or change the code and watch
the sheet follow in the REPL.
loadWorkbook(source)Edit a workbook you already have
Load an .xlsx or .xlsm from bytes, a file, a Blob, or a fetch response. Change the cells that change and save. Pivot tables, VBA projects, threaded comments, external links, and custom XML are carried through byte for byte.
Read the getting started guidecreateWorkbook()Build one from nothing
createWorkbook() returns an empty workbook with no template file to ship. Add sheets and rows, register a style once and reuse it by id, then add number formats, formulas, tables, data validation, conditional formatting, images, and charts.
Browse the recipescreateWriteOnlyWorkbook(sink)Stream the big ones
The write-only workbook deflates each row as you append it and pushes the bytes straight to the sink. The streaming reader walks a sheet with a SAX parser and yields one row at a time, so neither side holds the sheet in memory.
Read the streaming guide
Most spreadsheets that matter already exist
The report finance sends every month, the template with the pivot table, the .xlsm someone’s macros depend on. Load it, write the cells that change, and save. The parts the library does not model go back into the file exactly as they came out.
Reading is as typed as writing. A cell value is a discriminated union, not any; getCellDisplayText gives the text Excel shows under the cell’s
number format, and getCellDate reads a date serial through the workbook’s epoch.
import { loadWorkbook, workbookToBytes } from '@office-kit/xlsx/io';
import { fromBuffer } from '@office-kit/xlsx/node';
import { setCell } from '@office-kit/xlsx/worksheet';
import { readFile, writeFile } from 'node:fs/promises';
const wb = await loadWorkbook(fromBuffer(await readFile('input.xlsx')));
const ref = wb.sheets[0];
if (ref?.kind === 'worksheet') {
setCell(ref.sheet, 1, 1, 'Hello from @office-kit/xlsx');
}
await writeFile('output.xlsx', await workbookToBytes(wb));
A million rows without a million rows in memory
The streaming writer pushes each row through deflate as it arrives and forwards the chunk to the sink, so row buffering stays near 64 KiB however long the sheet gets. Shared strings are capped at 100,000 entries and 8 MiB; past that, new strings are written inline.
The reader inflates a worksheet chunk by chunk into a SAX parser, so the inflated sheet is never fully resident. One honest limit: ZIP needs its central directory, so the compressed archive itself is loaded up front.
import { toFile } from '@office-kit/xlsx/node';
import { createWriteOnlyWorkbook } from '@office-kit/xlsx/streaming';
const sink = toFile('big.xlsx');
const wb = await createWriteOnlyWorkbook(sink);
const ws = await wb.addWorksheet('Data');
ws.setColumnWidth(1, 24); // must precede the first appendRow
for (let r = 0; r < 1_000_000; r++) {
await ws.appendRow([r, `row-${r}`, r * Math.PI]);
}
await ws.close();
await wb.finalize();
Import the part you use
There is no root import. The package has 15 subpaths, every export has exactly one home, and nothing has side effects, so a bundler keeps only what you call. Sizes are minified and brotli-compressed with dependencies included, measured at 0.18.0; the cap is what CI enforces.
| Subpath | What it holds | Size | CI cap |
|---|---|---|---|
@office-kit/xlsx/io | Load and save a whole workbook | 107 KB | 120 KB |
@office-kit/xlsx/streaming | Row-by-row reader and append-only writer | 61 KB | 80 KB |
@office-kit/xlsx/styles | Fonts, fills, borders, number formats | 14 KB | 60 KB |
@office-kit/xlsx/worksheet | Cells, ranges, merges, tables, validation | 11 KB | 100 KB |
@office-kit/xlsx/workbook | Sheets, defined names, properties | 9 KB | 100 KB |
The other ten cover cells, charts, chartsheets, drawings, Node file helpers, and the packaging, schema, XML, ZIP, and utility layers. The API reference lists them all.
Valid files, checked by machines
“Excel happens to open it today” is not the bar. The bytes have to be valid OOXML, and a machine has to say so on every commit.
- Every kind of write is checked against the ECMA-376 schemas.
- A three-tier validator runs in CI: the OPC package structure, the ECMA-376 Transitional XSDs through xmllint, and the rules a schema cannot express, such as overlapping merges or a style index past the end of the table. A fast-check property test feeds it generated workbooks.
- What the library does not model, it does not touch.
- Pivot tables, VBA, OLE objects, external links, and custom XML pass through load and save byte for byte. Round-trip tests pin that against real workbooks, including the openpyxl fixture corpus.
- A bundle that grows too much fails the build.
- size-limit runs in CI on every push and pull request. The full load and save path has to stay under 120 KB minified and brotli-compressed with its dependencies, and the streaming entry under 80 KB.
- Two runtime dependencies.
- fflate for ZIP and saxes for streaming XML. Only the @office-kit/xlsx/node subpath touches fs; the rest is Web Streams, Blob, and Uint8Array.
- Tested where you run it, and as you install it.
- The suite runs on Linux, macOS, and Windows against Node 22, 24, and 26. A packaging job installs the packed tarball and compiles a consumer under node16, nodenext, and bundler resolution.
What you can build today
The library is pre-1.0 and says so. This is what works now, and what does not. The API reference lists every function.
- Cells
- Numbers, strings, booleans, errors, dates, and inline rich text. Normal, array, shared, and data-table formulas with cached values.
- Styles
- Fonts, fills, borders, alignment, protection, number formats, named styles, and differential styles, deduplicated into one pool.
- Sheets
- Merged cells, freeze panes, column widths and row heights, grouping, hidden rows and columns, page setup, sheet protection.
- Data tools
- Excel tables, autofilter, data validation, conditional formatting, and workbook- or sheet-scoped defined names.
- Reading
- A typed cell value union, the text Excel shows under a number format, dates read through the workbook epoch, and value extents.
- Charts
- 16 classic chart kinds and 8 modern ones such as sunburst, treemap, waterfall, and funnel, with trendlines, error bars, and chartsheets.
- Drawings
- PNG, JPEG, GIF, BMP, WebP, TIFF, SVG, EMF, and WMF images, with format and size detected from the bytes.
- Package
- Hyperlinks and comments in bulk, ZIP64 past 65,535 entries, deterministic output bytes, and macro-enabled .xlsm.
Not supported
- Formula evaluation. Cache the values you know, or ask Excel to recalculate on open
- Other formats: .xls, .xlsb, .ods, and .csv are out of scope
- ISO 29500 Strict workbooks are detected and named, not read
- Encrypted workbooks are detected, not decrypted
- Pivot table authoring (existing pivots are preserved)
- Charts, images, and tables in the streaming writer
Written to be driven by AI agents too
Spreadsheets are increasingly written by agents, so the docs are built for them as well as for you. Because every function has one import path, a model that knows the name knows the import.
/llms.txtA self-contained guide to the API, with an index of every docs page./llms-full.txtThe whole documentation in one file.any-docs-page.mdAdd .md to a docs URL to get the raw Markdown.
One kit, three file formats
Office Kit is a family of libraries built on the same rules: the ECMA-376 spec is the source of truth, output has to validate, and one ESM build has to run everywhere.
- .pptx @office-kit/pptx PowerPoint files. Slides, charts, tables, themes, notes, and animations. Open the pptx site
- .xlsx @office-kit/xlsx Excel files. Workbooks, formulas, styles, charts, and streaming for large sheets. You are here
- .docx @office-kit/docx Word files. Paragraphs, lists, tables, page setup, and template editing. Open the docx site