-
Notifications
You must be signed in to change notification settings - Fork 1
Table Layout Engine
Generate styled XLSX tables from declarative options -- no manual cell coordinate math.
The Table Layout Engine composes existing primitives (Worksheet.cell, StyleBuilder, addMergeCell, setColumnWidth, frozenPane) into a high-level API that handles header styling, zebra striping, auto-width, merge cells, per-cell overrides, and multi-table composition.
import { initWasm, Workbook, drawTable } from 'modern-xlsx';
await initWasm();
const wb = new Workbook();
const ws = wb.addSheet('Report');
const result = drawTable(wb, ws, {
headers: ['Name', 'Department', 'Salary'],
rows: [
['Alice', 'Engineering', 95000],
['Bob', 'Marketing', 72000],
['Carol', 'Engineering', 105000],
],
columnWidths: [20, 18, 12],
});
console.log(result.range); // "A1:C4"
await wb.toFile('report.xlsx');Draw a styled table on a worksheet.
| Parameter | Type | Description |
|---|---|---|
wb |
Workbook |
Workbook instance (needed for style registration) |
ws |
Worksheet |
Target worksheet |
opts |
DrawTableOptions |
Table configuration |
Returns a TableResult with layout metadata for composing multiple tables.
Create a table from a JSON array, auto-extracting headers from object keys.
| Parameter | Type | Description |
|---|---|---|
wb |
Workbook |
Workbook instance |
ws |
Worksheet |
Target worksheet |
data |
Record<string, unknown>[] |
Array of row objects |
opts |
DrawTableFromDataOptions |
Optional configuration |
const data = [
{ name: 'Alice', age: 30, city: 'NYC' },
{ name: 'Bob', age: 25, city: 'LA' },
];
drawTableFromData(wb, ws, data, {
headerMap: { name: 'Full Name', age: 'Age', city: 'City' },
autoWidth: true,
});Returned by both drawTable and drawTableFromData with layout metadata.
interface TableResult {
range: string; // "A1:D10" -- full table range (A1-style)
rowCount: number; // Total rows (header + data)
colCount: number; // Number of columns
firstDataRow: number; // 0-based row index of first data row
lastDataRow: number; // 0-based row index of last data row
}Full configuration for drawTable.
interface DrawTableOptions {
// --- Layout (required) ---
headers: string[];
rows: (string | number | boolean | null)[][];
// --- Layout (optional) ---
origin?: string; // A1-style origin cell (default: "A1")
columns?: TableColumn[]; // Per-column config
columnWidths?: number[]; // Fixed widths shorthand
autoWidth?: boolean; // Auto-calculate widths (default: false)
freezeHeader?: boolean; // Freeze the header row (default: false)
autoFilter?: boolean; // Add filter dropdowns (default: false)
wrapText?: boolean; // Enable text wrapping (default: false)
// --- Header styling ---
headerFont?: Partial<FontData>; // Default: bold white
headerColor?: string; // Hex, no # (default: '4472C4')
headerAlign?: 'left' | 'center' | 'right'; // Default: 'center'
// --- Body styling ---
bodyFont?: Partial<FontData>;
bodyAlign?: 'left' | 'center' | 'right';
verticalAlign?: 'top' | 'center' | 'bottom';
borderStyle?: BorderStyle | null; // Default: 'thin', null = none
borderColor?: string; // Hex (default: '000000')
alternateRowColor?: string | null; // Hex for zebra stripes
// --- Advanced ---
merges?: { row: number; col: number; rowSpan?: number; colSpan?: number }[];
cellStyles?: Record<string, CellStyle>;
}Extends DrawTableOptions (minus headers and rows) with JSON-specific options.
interface DrawTableFromDataOptions extends Omit<DrawTableOptions, 'headers' | 'rows'> {
headers?: string[]; // Explicit column order (default: Object.keys of first item)
headerMap?: Record<string, string>; // Map object keys to display headers
}Per-column configuration for width, alignment, and number format.
interface TableColumn {
header?: string; // Header label override
width?: number; // Fixed width (character units)
align?: 'left' | 'center' | 'right'; // Horizontal alignment for data cells
numberFormat?: string; // e.g. '#,##0.00', '$#,##0.00'
}Per-cell style override applied on top of the base table style.
interface CellStyle {
font?: Partial<FontData>;
fill?: { pattern?: string; fgColor?: string };
border?: Partial<{
left: { style: BorderStyle; color?: string | null };
right: { style: BorderStyle; color?: string | null };
top: { style: BorderStyle; color?: string | null };
bottom: { style: BorderStyle; color?: string | null };
}>;
alignment?: Partial<AlignmentData>;
numberFormat?: string;
}| Option | Type | Default | Description |
|---|---|---|---|
headers |
string[] |
required | Header labels |
rows |
(string|number|boolean|null)[][] |
required | Data rows |
origin |
string |
"A1" |
A1-style origin cell |
columns |
TableColumn[] |
-- | Per-column config (width, align, numberFormat) |
columnWidths |
number[] |
-- | Fixed column widths (shorthand for columns[].width) |
autoWidth |
boolean |
false |
Auto-calculate widths from content |
freezeHeader |
boolean |
false |
Freeze the header row |
autoFilter |
boolean |
false |
Add filter dropdown arrows to headers |
wrapText |
boolean |
false |
Enable text wrapping in body cells |
| Option | Type | Default | Description |
|---|---|---|---|
headerFont |
Partial<FontData> |
bold white | Header font |
headerColor |
string |
'4472C4' |
Header background color (hex) |
bodyFont |
Partial<FontData> |
-- | Body font override |
borderStyle |
BorderStyle|null |
'thin' |
Border style for all cells (null to disable) |
borderColor |
string |
'000000' |
Border color (hex) |
alternateRowColor |
string|null |
-- | Zebra stripe color for odd rows (hex) |
headerAlign |
'left'|'center'|'right' |
'center' |
Header horizontal alignment |
bodyAlign |
'left'|'center'|'right' |
-- | Body horizontal alignment |
verticalAlign |
'top'|'center'|'bottom' |
-- | Vertical alignment for all cells |
A financial table with currency formatting, frozen header, and zebra stripes.
drawTable(wb, ws, {
headers: ['Item', 'Qty', 'Unit Price', 'Total'],
rows: [
['Widget', 10, 25.5, 255],
['Gadget', 5, 42.0, 210],
['Doohickey', 2, 99.99, 199.98],
],
columnWidths: [20, 8, 12, 12],
headerColor: '2F5496',
alternateRowColor: 'D6E4F0',
freezeHeader: true,
columns: [
{ align: 'left' },
{ align: 'center' },
{ align: 'right', numberFormat: '$#,##0.00' },
{ align: 'right', numberFormat: '$#,##0.00' },
],
});Add alternating row colors for readability.
drawTable(wb, ws, {
headers: ['ID', 'Name', 'Status'],
rows: data,
alternateRowColor: 'F2F2F2',
});Override individual cell styles using "row,col" keys (0-based, relative to the data area).
drawTable(wb, ws, {
headers: ['Name', 'Score', 'Grade'],
rows: [
['Alice', 95, 'A'],
['Bob', 42, 'F'],
],
cellStyles: {
'1,1': { font: { color: 'FF0000', bold: true } },
'1,2': { fill: { pattern: 'solid', fgColor: 'FFCCCC' } },
},
});The cellStyles map uses "dataRow,col" keys where dataRow 0 is the first data row (row after headers). Overrides are merged on top of the base style for that cell (including zebra stripe and column styles).
Merge cells in the data area using 0-based row/col indices relative to the first data row.
drawTable(wb, ws, {
headers: ['Category', 'Product', 'Price'],
rows: [
['Electronics', 'Phone', 999],
['', 'Laptop', 1299],
['Clothing', 'Shirt', 49],
],
merges: [
{ row: 0, col: 0, rowSpan: 2 }, // Merge "Electronics" across 2 rows
],
});| Property | Type | Default | Description |
|---|---|---|---|
row |
number |
required | 0-based data row index |
col |
number |
required | 0-based column index |
rowSpan |
number |
1 |
Number of rows to span |
colSpan |
number |
1 |
Number of columns to span |
Use the returned TableResult metadata to position sequential tables.
const result1 = drawTable(wb, ws, {
headers: ['Q1 Summary'],
rows: [['Revenue: $1M'], ['Profit: $200K']],
});
// Place second table below the first, with a gap row
const nextRow = result1.lastDataRow + 2;
drawTable(wb, ws, {
headers: ['Q2 Summary'],
rows: [['Revenue: $1.2M'], ['Profit: $250K']],
origin: `A${nextRow + 1}`,
});Place multiple tables on the same row using column offsets.
drawTable(wb, ws, {
headers: ['Team A'],
rows: [['Alice'], ['Bob']],
origin: 'A1',
});
drawTable(wb, ws, {
headers: ['Team B'],
rows: [['Carol'], ['Dave']],
origin: 'D1', // Start 3 columns over
});Auto-width calculation counts CJK characters (codepoint > 0x2E7F) as double-width.
drawTable(wb, ws, {
headers: ['Name', 'Description'],
rows: [
['Widget', 'Standard component'],
['部品', '日本語の説明'],
],
autoWidth: true,
});Use drawTableFromData to skip manual header/row extraction.
const employees = [
{ name: 'Alice', department: 'Engineering', salary: 95000 },
{ name: 'Bob', department: 'Marketing', salary: 72000 },
{ name: 'Carol', department: 'Design', salary: 88000 },
];
drawTableFromData(wb, ws, employees, {
headerMap: {
name: 'Employee Name',
department: 'Department',
salary: 'Annual Salary',
},
columns: [
{ align: 'left' },
{ align: 'center' },
{ align: 'right', numberFormat: '$#,##0' },
],
autoWidth: true,
alternateRowColor: 'F5F5F5',
freezeHeader: true,
autoFilter: true,
});Combine multiple features into a production-ready report.
import { initWasm, Workbook, drawTable, drawTableFromData } from 'modern-xlsx';
await initWasm();
const wb = new Workbook();
const ws = wb.addSheet('Monthly Report');
// Title table
const titleResult = drawTable(wb, ws, {
headers: ['March 2026 Sales Report'],
rows: [],
headerColor: '1F4E79',
headerFont: { bold: true, size: 16, color: 'FFFFFF' },
columnWidths: [60],
borderStyle: null,
});
// Data table below title
const dataRow = titleResult.lastDataRow + 2;
drawTable(wb, ws, {
headers: ['Region', 'Q1', 'Q2', 'Q3', 'Q4', 'Total'],
rows: [
['North', 120000, 135000, 142000, 155000, 552000],
['South', 95000, 102000, 98000, 115000, 410000],
['East', 88000, 91000, 105000, 112000, 396000],
['West', 145000, 152000, 161000, 170000, 628000],
],
origin: `A${dataRow + 1}`,
headerColor: '2F5496',
alternateRowColor: 'D6E4F0',
freezeHeader: false,
autoFilter: true,
columns: [
{ align: 'left', width: 14 },
{ align: 'right', numberFormat: '$#,##0', width: 12 },
{ align: 'right', numberFormat: '$#,##0', width: 12 },
{ align: 'right', numberFormat: '$#,##0', width: 12 },
{ align: 'right', numberFormat: '$#,##0', width: 12 },
{ align: 'right', numberFormat: '$#,##0', width: 14 },
],
cellStyles: {
'3,5': { font: { bold: true, color: '1F4E79' } }, // Highlight West total
},
});
await wb.toFile('monthly-report.xlsx');-
Style palette --
drawTablepre-builds a fixed set of style indices (header, body-even, body-odd, per-column variants) usingStyleBuilder. This avoids creating duplicate styles for every cell. -
Header row -- Headers are written at the origin row with the header style.
-
Data rows -- Each cell gets the appropriate style based on: per-column config > zebra stripe > base body style. Per-cell overrides (
cellStyles) are merged on top. -
Column widths -- Either from
columnWidths,columns[].width, or auto-calculated from content (with CJK double-width support). -
Post-processing -- Merge cells, frozen pane, and auto-filter are applied after all cells are written.
-
Result -- Returns
TableResultwith the range and row indices for composing additional tables.
- Style efficiency -- The engine builds styles once per table region, not per cell. Even a 10,000-row table creates only a handful of unique styles.
-
originpositioning -- UseTableResult.lastDataRowto chain tables vertically. Use column letters to place tables side by side. -
columnWidthsvscolumns-- UsecolumnWidthsfor quick fixed widths. Usecolumnswhen you also need alignment or number format per column. -
autoWidth-- Scans all header and data values to compute widths. Suitable for small/medium tables. For very large tables, prefer explicitcolumnWidths. -
cellStyleskeys -- Keys are"row,col"strings where row 0 is the first data row (not the header). Column 0 is the first column of the table.
modern-xlsx v1.0.0
Getting Started
Guides
- Charts & Visualizations
- Formula Engine
- Table Layout Engine
- Tables & Print Layout
- Encryption
- Feature Comparison
Reference
Migration
Project