Styles and number formats
A cell's appearance is six independent facets: font, fill, border, alignment, numFmt and protection. Each is set on its own, each is absent until you set it, and an absent facet is genuinely absent rather than an empty placeholder.
import {Workbook} from '@shbernal/ts-xlsx';
const sheet = new Workbook().addWorksheet('Report');
const cell = sheet.getCell('A1');
cell.value = 'Total';
cell.font = {bold: true, size: 12, color: {argb: 'FFFFFFFF'}};
cell.fill = {type: 'pattern', pattern: 'solid', fgColor: {argb: 'FF146C43'}};
cell.alignment = {horizontal: 'center', vertical: 'center'};
console.log(cell.font?.bold); // true
console.log(cell.border); // undefinedColour is an ARGB hex string, uppercase, alpha first, no leading #. FF is opaque, so opaque white is FFFFFFFF and opaque black is FF000000.
Styling more than one cell
Setting a facet on a Range sets it on every cell in the range, and setting one on a Column or Row sets the default for that line. They are separate mechanisms: a column style applies to cells the column does not otherwise have, while a range style is written onto each cell.
import {Workbook} from '@shbernal/ts-xlsx';
const sheet = new Workbook().addWorksheet('Report');
sheet.addRow(['Region', 'Revenue']);
sheet.addRow(['North', 84200]);
sheet.addRow(['South', 61050]);
sheet.getRange('A1:B1').font = {bold: true};
sheet.getColumn(2).numFmt = '#,##0';
console.log(sheet.getCell('B1').font?.bold); // true
console.log(sheet.getColumn(2).numFmt); // '#,##0'Facets do not leak between cells. Setting a border on B2 leaves B3 alone, even though the two may share a style record underneath; the sharing is a storage detail the model does not let you observe.
Number formats
A number format is a string, and the library carries it verbatim. It is not parsed, not normalised, and not validated, which is the only behaviour that lets an author use a format this library has never heard of.
import {readXlsx, Workbook, writeXlsx} from '@shbernal/ts-xlsx';
const accounting = '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)';
const source = new Workbook();
const built = source.addWorksheet('Money');
built.getCell('A1').value = 1234.5;
built.getCell('A1').numFmt = accounting;
built.getCell('A2').value = new Date('2026-03-04');
built.getCell('A2').numFmt = 'yyyy-mm-dd';
built.getCell('A3').value = 0.62;
built.getCell('A3').numFmt = '0.0%';
const sheet = readXlsx(writeXlsx(source)).requireWorksheet('Money');
console.log(sheet.getCell('A1').numFmt === accounting); // trueThat round trip is not a coincidence. It is a regression case (custom-numfmt-string-roundtrips-verbatim), because a format code silently losing its group separators renders every cell in a column wrong and nothing throws.
The library does not apply formats. It stores them, round-trips them, and hands them to whatever opens the file. cell.text and cellValueToText give you the value's text with no format applied, so a currency cell has no currency sign and 0.1 + 0.2 reads as 0.30000000000000004. If you need the string Excel would paint, you need a formatting engine, and this is not one. The playground shows the same distinction on screen.
Rich text, when one cell holds two fonts
import {readXlsx, richTextToPlain, Workbook, writeXlsx} from '@shbernal/ts-xlsx';
const source = new Workbook();
source.addWorksheet('Note').getCell('A1').value = {
richText: [
{text: 'Warning: ', font: {bold: true, color: {argb: 'FF8A5300'}}},
{text: 'the total is provisional.'},
],
};
const sheet = readXlsx(writeXlsx(source)).requireWorksheet('Note');
const value = sheet.getCell('A1').value;
console.log(typeof value === 'object' && value !== null && 'richText' in value); // true
console.log(richTextToPlain({richText: [{text: 'a'}, {text: 'b'}]})); // 'ab'richTextToPlain is what a consumer that cannot render per-run formatting sees, which is also what a CSV field or a pivot cache entry gets.
Themes and the default font
A workbook carries a theme: a colour scheme and a font scheme that styles refer to by role rather than by value. Reading a file keeps the theme it came with, and resolveColor turns a theme reference into the ARGB it currently means.
import {Workbook} from '@shbernal/ts-xlsx';
const workbook = new Workbook();
console.log(workbook.defaultFont.name !== undefined); // true
console.log(workbook.themeColors.accent1 !== undefined); // trueThe default font is declared rather than assumed. That matters more than it sounds: column width in OOXML is measured in characters of the workbook's default font, so a library that guessed the font would compute the wrong widths for any workbook that set one. ADR-0025 has the case.
Next: structure for the geometry around the values.