Skip to content

writeXlsx and writeXlsxStream format a styled cell in a formatted column differently #616

Closed

Description

@ronistrom

What happens

A column with a number format, and a cell in it with its own style that doesn't mention a number format:

const columns = [{ numFmt: "0.00" }]
const own = { font: { bold: true } }

// writeXlsx
writeXlsx({ sheets: [{ name: "S", columns, rows: [[1.5]], cells: new Map([["0,0", { value: 1.5, style: own }]]) }] })

// writeXlsxStream
writeXlsxStream([[{ value: 1.5, style: own }]], { name: "S", columns })

The two writers give A1 different formats:

writer A1's xf Excel shows
writeXlsx bold, numFmtId="0" (General) 1.5
writeXlsxStream bold, numFmtId="164" (0.00) 1.50

writeXlsx uses the cell's own style as is, so the column's format is dropped. The stream writer's RowSerializer.serializeRow adds the column's numFmt to any style that has none, but not the column's font, fill or anything else.

Expected

Both writers resolve a cell's format the same way. Which way is right is a design choice:

  • Own style replaces the column's (what writeXlsx does): simple, but a bold cell in a currency column loses its currency format.
  • Own style layered over the column's: closer to Excel, where a cell in a formatted column keeps the column's format when you make it bold. The stream writer half-does this, for numFmt only.

Repro (main at c991ad0)

Write the two workbooks above and compare <c r="A1" s="…"> and its xf in xl/styles.xml.

Where

resolveRows / columnCellStyle in src/xlsx/worksheet-writer.ts, and RowSerializer.serializeRow in src/xlsx/stream-writer.ts.

Note

#612 (row formats) adds a row style between the cell and the column. It keeps this existing difference as it is, rather than half-fixing it there.

Activity

  1. ronistrom commented on Oct 21, 2026

    @ronistrom
    ContributorAuthor

    A second difference between the two writers, also about column formats: writeXlsxStream never writes a column's style onto <col>. The cells it writes do get the style, so the rows you streamed look right. But a cell typed into the column later doesn't get the format, and the column comes back without its style when the file is read.

    const columns = [{ width: 12, style: { numFmt: '"$"#,##0.00' } }]
    writeXlsx({ sheets: [{ name: "S", columns, rows: [[1.5]] }] })
    writeXlsxStream([[1.5]], { name: "S", columns })
    writer <col> A1 read back with readStyles: true
    writeXlsx <col min="1" max="1" width="12" customWidth="true" style="1"/> s="1" { width: 12, style: { numFmt: "\"$\"#,##0.00" } }
    writeXlsxStream <col min="1" max="1" width="12" customWidth="true"/> s="1" { width: 12 }

    main at c991ad0. buildSheetPrelude in src/xlsx/stream-writer.ts writes width, hidden and outlineLevel on <col> but not style, and it writes no <col> at all for a column with only a style or a numFmt. The buffered writer has done both since #439 §W. CodeRabbit raised this while reviewing #614. That PR only touches the <col> width, so it leaves this alone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions