modern-xlsx
v1.0.0
Published
High-performance XLSX read/write library powered by Rust WASM. Full styling, encryption, data validation, conditional formatting — features SheetJS charges for, free and open source.
Maintainers
Readme
Full cell styling, data validation, conditional formatting, frozen panes, hyperlinks, comments, sheet protection, encryption, and more — features that SheetJS locks behind a paid Pro license — all free and open source.
Install
npm install modern-xlsxRequires a runtime with WASM support (Node.js 24+, Bun, Deno, modern browsers).
Quick Start
import { initWasm, Workbook } from 'modern-xlsx';
await initWasm();
const wb = new Workbook();
wb.addSheet('Sheet1').cell('A1').value = 'Hello, Excel!';
await wb.toFile('output.xlsx');Create a Styled Workbook
import { initWasm, Workbook, readFile } from 'modern-xlsx';
await initWasm();
const wb = new Workbook();
const ws = wb.addSheet('Sales');
ws.cell('A1').value = 'Product';
ws.cell('B1').value = 'Revenue';
ws.cell('A2').value = 'Widget';
ws.cell('B2').value = 9999.99;
// Style headers
const header = wb.createStyle()
.font({ bold: true, size: 14, color: '1F4E79' })
.fill({ pattern: 'solid', fgColor: 'D6E4F0' })
.alignment({ horizontal: 'center' })
.border({ bottom: { style: 'medium', color: '1F4E79' } })
.build(wb.styles);
ws.cell('A1').styleIndex = header;
ws.cell('B1').styleIndex = header;
await wb.toFile('sales.xlsx');
// Read an existing file
const existing = await readFile('report.xlsx');
console.log(existing.getSheet('Sheet1')?.cell('A1').value);Performance
Benchmarks on Node.js (single thread, v1.0.0):
| Operation | modern-xlsx | SheetJS CE | Factor |
|-----------|------------:|-----------:|-------:|
| Read 100K rows | 472 ms | 1,901 ms | 4.0x faster |
| Read 10K rows | 69 ms | 170 ms | 2.5x faster |
| Write 100K (batch aoaToSheet) | 232 ms | 1,950 ms | 8.4x faster |
| Write 50K (batch aoaToSheet) | 49 ms | 80 ms | 1.6x faster |
| Write 10K (cell-by-cell) | 175 ms | 125 ms | 0.7x |
| sheetToCsv (10K) | 37 ms | 31 ms | ~1.0x |
| sheetToJson (10K) | 36 ms | 22 ms | ~0.6x |
Summary: modern-xlsx is 4-8x faster for bulk read/write (its primary use case). SheetJS is faster for cell-by-cell writes and small utility conversions. For large workbooks, the WASM-accelerated Rust core delivers significant throughput gains.
ESM 133 KB + IIFE 60 KB + WASM 1.1 MB. Zero runtime dependencies.
Feature Comparison
| Feature | modern-xlsx | SheetJS CE | SheetJS Pro | |---------|:-----------:|:----------:|:-----------:| | Read / write XLSX | Yes | Yes | Yes | | Cell styling (fonts, fills, borders) | Free | No | Paid | | Number formats | Free | Read only | Paid | | Alignment & text wrap | Free | No | Paid | | Cell protection | Free | No | Paid | | Comments / notes | Free | Read only | Paid | | Rich text (inline formatting) | Free | No | Paid | | Data validation | Free | No | Paid | | Conditional formatting | Free | No | Paid | | Frozen panes | Free | Partial | Paid | | Hyperlinks | Free | Yes | Yes | | Auto filter | Free | Basic | Yes | | Sheet protection | Free | Read only | Paid | | Page setup & margins | Free | Margins only | Paid | | Temporal API dates | Yes | No | No | | Format cell values (SSF) | Free | Basic | Full | | Available on npm | Yes | Yes | No | | Tree-shakable ESM | Yes | No | No | | Strict TypeScript types | Yes | Partial | Partial | | WASM-accelerated I/O | Yes | No | No | | OOXML validation & repair | Yes | No | No | | Barcode & QR code generation | Yes | No | No | | Image embedding | Yes | No | Paid | | Excel Tables (ListObjects) | Yes | No | Paid | | Headers & footers | Yes | No | Paid | | Row/column grouping (outline) | Yes | No | Paid | | Print titles & areas | Yes | No | Paid | | Encryption (password protection) | Yes | No | No | | Charts (bar, line, pie, etc.) | Yes | No | Paid | | Formula engine (54 functions) | Yes | No | No | | Sparklines | Yes | No | Paid | | Pivot tables (read/write) | Yes | No | Paid | | Threaded comments | Yes | No | No | | Slicers (read/write) | Yes | No | No | | Timelines (read/write) | Yes | No | No | | Streaming writer (100K+ rows) | Yes | No | Paid | | CLI tool (info/convert) | Yes | No | No | | Feature-gated WASM | Yes | No | No |
How It Works
┌────────────────────────────┐
TypeScript API │ Workbook / Worksheet / Cell │
└─────────────┬──────────────┘
│ JSON
┌─────────────▼──────────────┐
WASM boundary │ wasm-bindgen bridge │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
Rust core │ OOXML parser & writer │
│ (quick-xml + zip) │
└────────────────────────────┘Data crosses the WASM boundary as a JSON string — 8-13x faster than serde_wasm_bindgen for large workbooks. The Rust core (Rust 1.95, Edition 2024) handles all ZIP compression, XML parsing, shared string tables, and style resolution. Error paths use cold_path() hints for optimal branch layout, and hot-path helpers are #[inline]-annotated for maximum throughput.
API Reference
Initialization
import { initWasm } from 'modern-xlsx';
await initWasm(); // call once before any operationReading
import { readFile, readBuffer } from 'modern-xlsx';
const wb = await readFile('data.xlsx'); // Node.js / Bun / Deno
const wb = await readBuffer(uint8Array); // any environmentWriting
await wb.toFile('output.xlsx'); // Node.js / Bun / Deno
const buffer = await wb.toBuffer(); // Uint8Array
import { writeBlob } from 'modern-xlsx';
const blob = writeBlob(wb); // browser BlobWorkbook
const wb = new Workbook();
wb.sheetNames; // string[]
wb.sheetCount; // number
wb.dateSystem; // 'date1900' | 'date1904'
wb.styles; // StylesData
wb.addSheet('Name'); // Worksheet
wb.getSheet('Name'); // Worksheet | undefined
wb.getSheetByIndex(0); // Worksheet | undefined
wb.removeSheet('Name'); // boolean
wb.removeSheet(0); // boolean
// Named ranges
wb.addNamedRange('MyRange', 'Sheet1!$A$1:$D$10');
wb.getNamedRange('MyRange');
wb.removeNamedRange('MyRange');
// Document properties
wb.docProperties = { title: 'Report', creator: 'App' };
// Workbook views
wb.workbookViews = [{
activeTab: 0,
firstSheet: 0,
showHorizontalScroll: true,
showVerticalScroll: true,
showSheetTabs: true,
}];Worksheet
const ws = wb.addSheet('Sheet1');
// Cell access
ws.cell('A1').value = 'Hello';
ws.cell('B1').value = 42;
ws.cell('C1').value = true;
ws.cell('D1').formula = 'SUM(B1:B100)';
// Columns & rows
ws.setColumnWidth(1, 20);
ws.setRowHeight(1, 30);
ws.setRowHidden(2, true);
// Merge cells
ws.addMergeCell('A1:D1');
ws.removeMergeCell('A1:D1');
// Frozen panes
ws.frozenPane = { rows: 1, cols: 0 };
// Auto filter
ws.autoFilter = 'A1:D100';
ws.autoFilter = {
range: 'A1:D100',
filterColumns: [{ colId: 0, filters: ['Yes'] }],
};
// Hyperlinks
ws.addHyperlink('A1', '#Sheet2!A1', {
display: 'Go to Sheet2',
tooltip: 'Click',
});
ws.removeHyperlink('A1');
// Data validation
ws.addValidation('B2:B100', {
validationType: 'list',
operator: null,
formula1: '"Yes,No,Maybe"',
formula2: null,
allowBlank: true,
showErrorMessage: true,
errorTitle: 'Invalid',
errorMessage: 'Pick from list',
});
// Comments
ws.addComment('A1', 'Author', 'This is a comment');
ws.removeComment('A1');
// Threaded comments (modern Excel)
const commentId = ws.addThreadedComment('A1', 'Discussion point', 'Alice');
ws.replyToComment(commentId, 'I agree', 'Bob');
ws.threadedComments; // readonly ThreadedCommentData[]
// Pivot tables (read/write)
ws.pivotTables; // readonly PivotTableData[]
ws.addPivotTable({ name: 'Sales', cacheId: 0, location: { ref: 'A3:D20' }, pivotFields: [], rowFields: [], colFields: [], dataFields: [], pageFields: [] });
ws.removePivotTable(0); // boolean
// Slicers & timelines (read/write)
ws.slicers; // readonly SlicerData[]
ws.addSlicer({ name: 'Slicer_Region', cacheName: 'Slicer_Region' });
ws.removeSlicer(0); // boolean
ws.timelines; // readonly TimelineData[]
ws.addTimeline({ name: 'Timeline_Date', cacheName: 'NativeTimeline_Date' });
ws.removeTimeline(0); // boolean
// Page setup & margins
ws.pageSetup = { orientation: 'landscape', paperSize: 9 };
ws.pageMargins = {
top: 0.75, bottom: 0.75,
left: 0.7, right: 0.7,
header: 0.3, footer: 0.3,
};
// Sheet protection
ws.sheetProtection = {
sheet: true, objects: false, scenarios: false,
formatCells: false, formatColumns: false, formatRows: false,
insertColumns: false, insertRows: false,
deleteColumns: false, deleteRows: false,
sort: false, autoFilter: false,
};
// Excel Tables (ListObjects)
ws.addTable({
name: 'SalesData', ref: 'A1:B3',
columns: [{ name: 'Product' }, { name: 'Revenue' }],
style: { name: 'TableStyleMedium9', showRowStripes: true },
});
ws.tables; // TableDefinitionData[]
ws.getTable('SalesData'); // TableDefinitionData | undefined
ws.removeTable('SalesData'); // boolean
// Headers & Footers
import { HeaderFooterBuilder } from 'modern-xlsx';
ws.headerFooter = {
oddHeader: new HeaderFooterBuilder()
.left(HeaderFooterBuilder.date())
.center(HeaderFooterBuilder.bold('Report'))
.right(`Page ${HeaderFooterBuilder.pageNumber()}`)
.build(),
};
// Row & Column Grouping
ws.groupRows(2, 10); // outline level 1
ws.groupRows(3, 5, 2); // nested level 2
ws.collapseRows(2, 10);
ws.expandRows(2, 10);
ws.groupColumns(1, 3); // columns A-C
ws.outlineProperties = { summaryBelow: true, summaryRight: true };
// Print Titles & Areas
wb.setPrintTitles('Sheet1', { rows: { start: 1, end: 1 } });
wb.setPrintArea('Sheet1', 'A1:G50');Charts
Create Excel charts with the fluent ChartBuilder API:
import { ChartBuilder } from 'modern-xlsx';
const ws = wb.addSheet('Sales');
ws.cell('A1').value = 'Q1'; ws.cell('A2').value = 'Q2';
ws.cell('B1').value = 100; ws.cell('B2').value = 200;
// Create a bar chart
const chart = new ChartBuilder('bar')
.title('Quarterly Sales')
.addSeries({ name: 'Revenue', values: 'Sales!$B$1:$B$4' })
.categoryAxis({ title: 'Quarter' })
.valueAxis({ title: 'Amount ($)' })
.size(800, 400)
.build();
ws.addChart(chart);
// Supported types: bar, column, line, pie, area, scatter, radar, doughnut, bubble, stockFormula Engine
Evaluate 54 built-in Excel functions in-memory:
import { evaluateFormula, createDefaultFunctions } from 'modern-xlsx';
const ctx = {
getCell: (sheet, col, row) => wb.getSheet(sheet)?.cell(encodeCellRef(row - 1, letterToColumn(col)))?.value ?? null,
currentSheet: 'Sheet1',
functions: createDefaultFunctions(),
};
const result = evaluateFormula('SUM(A1:A10)', ctx);
// Supports: SUM, AVERAGE, COUNT, IF, VLOOKUP, INDEX/MATCH, CONCATENATE, and 46 moreEncryption
Read and write password-protected XLSX files (ECMA-376 Standard Encryption):
// Read encrypted file
const wb = await readFile('protected.xlsx', { password: 'secret' });
// Write encrypted file
await wb.toFile('output.xlsx', { password: 'secret' });Styles
Fluent builder that produces a style index for any cell:
const idx = wb.createStyle()
.font({ name: 'Arial', size: 12, bold: true, color: 'FF0000' })
.fill({ pattern: 'solid', fgColor: 'FFFF00' })
.border({
top: { style: 'thin', color: '000000' },
bottom: { style: 'double', color: '000000' },
left: { style: 'thin', color: '000000' },
right: { style: 'thin', color: '000000' },
})
.alignment({ horizontal: 'center', vertical: 'top', wrapText: true, textRotation: 45 })
.protection({ locked: true, hidden: false })
.numberFormat('#,##0.00')
.build(wb.styles);
ws.cell('A1').styleIndex = idx;Utilities
import {
aoaToSheet, jsonToSheet, sheetToJson, sheetToCsv, sheetToHtml,
sheetAddAoa, sheetAddJson,
dateToSerial, serialToDate, isDateFormatId, isDateFormatCode,
formatCell,
encodeCellRef, decodeCellRef, encodeRange, decodeRange,
columnToLetter, letterToColumn,
} from 'modern-xlsx';
// Array-of-arrays -> sheet
const ws = aoaToSheet([
['Name', 'Age'],
['Alice', 30],
['Bob', 25],
]);
// JSON -> sheet
const ws2 = jsonToSheet([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
]);
// Sheet -> JSON / CSV / HTML
const data = sheetToJson(ws); // [{ Name: 'Alice', Age: 30 }, ...]
const csv = sheetToCsv(ws); // "Name,Age\nAlice,30\nBob,25"
const html = sheetToHtml(ws); // "<table>..."
// Append to existing sheet
sheetAddAoa(ws, [['Charlie', 35]], { origin: 'A4' });
sheetAddJson(ws, [{ name: 'Diana', age: 28 }]);
// Limit rows
const first5 = sheetToJson(ws, { sheetRows: 5 });
// Date conversion (Date and Temporal API)
const serial = dateToSerial(new Date(2024, 0, 1));
const serial2 = dateToSerial({ year: 2024, month: 1, day: 1 }); // Temporal-like
const date = serialToDate(45292);
// Format cell value using Excel format code
formatCell(45292, 'yyyy-mm-dd'); // "2024-01-01"
// Cell references
decodeCellRef('B3'); // { row: 2, col: 1 }
encodeCellRef(2, 1); // "B3"Validation & Repair
WASM-accelerated OOXML compliance checking and auto-repair:
const wb = new Workbook();
const ws = wb.addSheet('Sheet1');
ws.cell('A1').value = 'Hello';
ws.cell('A1').styleIndex = 999; // dangling!
// Validate — returns a structured report
const report = wb.validate();
console.log(report.isValid); // false
console.log(report.errorCount); // 1
console.log(report.issues[0]);
// {
// severity: 'error',
// category: 'styleIndex',
// message: 'Cell A1 styleIndex=999 exceeds cellXfs count (1)',
// location: 'Sheet1!A1',
// suggestion: 'Clamp styleIndex to 0',
// autoFixable: true
// }
// Repair — auto-fixes all repairable issues
const { workbook, report: postReport, repairCount } = wb.repair();
console.log(repairCount); // 2 (style index + missing theme)
console.log(postReport.isValid); // trueDetects: dangling style/font/fill/border indices, overlapping merges, invalid cell refs, duplicate sheet names, bad metadata dates, missing required styles, missing theme colors, unsorted rows, SharedString cells without values.
Barcode & QR Code Generation
Generate barcodes and embed them as images — pure TypeScript, zero dependencies:
import { Workbook, generateBarcode, encodeQR, renderBarcodePNG } from 'modern-xlsx';
const wb = new Workbook();
wb.addSheet('Labels');
// High-level: generate + embed in one call
wb.addBarcode('Labels',
{ fromCol: 0, fromRow: 0, toCol: 4, toRow: 4 },
'https://example.com',
{ type: 'qr', ecLevel: 'M' },
);
// Or use the low-level pipeline:
const matrix = encodeQR('Hello', { ecLevel: 'H' });
const png = renderBarcodePNG(matrix, { moduleSize: 6, showText: true, textValue: 'Hello' });
wb.addImage('Labels', { fromCol: 5, fromRow: 0, toCol: 9, toRow: 4 }, png);Supported formats: QR Code, Code 128, EAN-13, UPC-A, Code 39, PDF417, Data Matrix, ITF-14, GS1-128.
See the Barcode Guide for format comparison and usage details.
CLI Tool
# Show sheet info
npx modern-xlsx info report.xlsx
# Convert to JSON
npx modern-xlsx convert report.xlsx output.json
# Export single sheet as CSV
npx modern-xlsx convert report.xlsx sheet1.csv --sheet 0 --format csvError Handling
import { ModernXlsxError } from 'modern-xlsx';
try {
ws.replyToComment('nonexistent-id', 'text', 'author');
} catch (e) {
if (e instanceof ModernXlsxError) {
console.log(e.code); // 'COMMENT_NOT_FOUND'
console.log(e.message); // 'Comment nonexistent-id not found'
}
}Rich Text
import { RichTextBuilder } from 'modern-xlsx';
const runs = new RichTextBuilder()
.text('Normal ')
.bold('Bold ')
.italic('Italic ')
.styled('Custom', { color: 'FF0000', fontSize: 14 })
.build();Browser Usage (ESM)
<script type="module">
import { initWasm, Workbook, writeBlob } from 'modern-xlsx';
await initWasm();
const wb = new Workbook();
wb.addSheet('Sheet1').cell('A1').value = 'Hello from browser!';
const blob = writeBlob(wb);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'output.xlsx';
a.click();
</script>Browser Usage (CDN / IIFE)
Single <script> tag — no bundler required:
<script src="https://cdn.jsdelivr.net/npm/modern-xlsx@latest/dist/modern-xlsx.min.js"></script>
<script>
(async () => {
await ModernXlsx.initWasm();
const wb = new ModernXlsx.Workbook();
wb.addSheet('Sheet1').cell('A1').value = 'Hello!';
const blob = ModernXlsx.writeBlob(wb);
// trigger download...
})();
</script>Also available via unpkg: https://unpkg.com/modern-xlsx@latest/dist/modern-xlsx.min.js
Web Worker (Off-Thread)
Keep the main thread responsive by running XLSX operations in a Web Worker:
import { createXlsxWorker } from 'modern-xlsx';
const worker = createXlsxWorker({
workerUrl: '/modern-xlsx.worker.js',
wasmUrl: '/modern-xlsx.wasm', // optional
});
const data = await worker.readBuffer(xlsxBytes);
const output = await worker.writeBuffer(data);
worker.terminate();Lazy Initialization
Auto-initialize WASM on first use:
import { ensureReady, Workbook } from 'modern-xlsx';
await ensureReady(); // no-op if already initialized
const wb = new Workbook();Custom WASM URL
For environments where auto-detection doesn't work:
import { initWasm } from 'modern-xlsx';
// Custom URL
await initWasm('https://my-cdn.com/modern-xlsx.wasm');
// From fetch Response
const res = await fetch('/wasm/modern-xlsx.wasm');
await initWasm(res);Types
All types are exported and include full TypeScript definitions:
import type {
WorkbookData, WorksheetData, SheetData, CellData, RowData,
StylesData, FontData, FillData, BorderData, AlignmentData, ProtectionData,
CellXfData, NumFmt, ColumnInfo, FrozenPane,
AutoFilterData, FilterColumnData, HyperlinkData,
DataValidationData, ConditionalFormattingData,
PageSetupData, SheetProtectionData,
DocPropertiesData, ThemeColorsData, WorkbookViewData,
RichTextRun, DefinedNameData, CalcChainEntryData,
// Validation & compliance
ValidationReport, ValidationIssue, Severity, IssueCategory, RepairResult,
// Barcode & image embedding
BarcodeMatrix, DrawBarcodeOptions, RenderOptions, ImageAnchor,
// Tables & print layout
TableDefinitionData, TableColumnData, TableStyleInfoData,
HeaderFooterData, OutlinePropertiesData,
// Charts
ChartDataModel, ChartSeriesData, ChartAxisData, ChartType,
ChartAnchorData, ChartGrouping, ChartLegendData,
// Formula engine
ASTNode, CellValue, EvalContext, FormulaFunction,
// Pivot tables & caches
PivotTableData, PivotFieldData, PivotDataFieldData, PivotAxis, SubtotalFunction,
PivotCacheDefinitionData, PivotCacheRecordsData, CacheFieldData, CacheSource, CacheValue,
// Threaded comments
ThreadedCommentData, PersonData,
// Slicers & timelines
SlicerData, SlicerCacheData, TimelineData, TimelineCacheData,
// Streaming writer
StreamingXlsxWriter, StreamingCellInput,
} from 'modern-xlsx';Pivot Tables
const wb = await readAsync(buffer);
const ws = wb.getSheet('PivotReport');
// Read pivot table metadata
for (const pt of ws.pivotTables) {
console.log(pt.name, pt.location);
}
// Add a pivot table
ws.addPivotTable({
name: 'SalesPivot', cacheId: 0,
location: { ref: 'A3:D20' },
pivotFields: [{ items: [], name: 'Region', axis: 'axisRow' }],
rowFields: [{ x: 0 }], colFields: [], dataFields: [], pageFields: [],
});
// Workbook-level pivot caches
wb.addPivotCache({ source: { ref: 'A1:D100', sheet: 'Data' } });Threaded Comments
const ws = wb.getSheet('Sheet1');
// Add a threaded comment
ws.addThreadedComment('A1', 'Review this value', 'Alice');
ws.replyToComment('A1', 'Looks correct to me', 'Bob');
// Read threaded comments
for (const tc of ws.threadedComments) {
console.log(tc.ref, tc.text, tc.author);
}Slicers & Timelines
const ws = wb.getSheet('Dashboard');
// Read slicers & timelines
for (const slicer of ws.slicers) console.log(slicer.name, slicer.caption);
for (const tl of ws.timelines) console.log(tl.name, tl.caption);
// Add a slicer
ws.addSlicer({ name: 'Slicer_Region', cacheName: 'Slicer_Region', caption: 'Region' });
wb.addSlicerCache({ name: 'Slicer_Region', sourceName: 'Region', items: [{ n: 'East', s: true }, { n: 'West', s: false }] });
// Add a timeline
ws.addTimeline({ name: 'Timeline_Date', cacheName: 'NativeTimeline_Date', caption: 'Date', level: 'months' });
wb.addTimelineCache({ name: 'NativeTimeline_Date', sourceName: 'Date', selectionStart: '2024-01-01', selectionEnd: '2024-12-31' });Streaming Writer (Large Files)
import { initWasm, StreamingXlsxWriter } from 'modern-xlsx';
await initWasm();
const writer = StreamingXlsxWriter.create();
writer.startSheet('Data');
for (let i = 0; i < 100_000; i++) {
writer.writeRow([
{ value: String(i), cellType: 'number' },
{ value: `row_${i}`, cellType: 'sharedString' },
]);
}
const xlsx: Uint8Array = writer.finish();CLI Tool
# Get workbook info
modern-xlsx info report.xlsx
# Convert to JSON
modern-xlsx convert report.xlsx output.json
# Convert to CSV
modern-xlsx convert report.xlsx output.csv