Escape
HomeDocsThe XLSX target

The XLSX target

Intent

@quario/xlsx flattens the banded walk onto one worksheet and writes the values as native cells. A recipient who opens the file can sort it, filter it, and total a column. That’s the whole reason to send a workbook rather than a PDF.

Design decisions

A grid isn’t a page, so page concepts go unread. Page bands, break: "page", reset: "page", page.margin, a pinned header height, and page columns all do nothing here. That’s not missing work. A worksheet has no page to strip or pin from.

Column widths aren’t read either, and that’s the same argument. A width is a share of the table width, and a worksheet’s columns are global to the sheet. Sizing against a notional sheet width would resolve your percentage against a number you never wrote.

Chrome merges, data doesn’t. A spanning cell in a header row or a total row goes into the first column it covers, and the target merges the range. The data region never merges, so sort, filter, and column selection keep working over it.

String cells are never formulas. Text beginning with = arrives as inert literal text with no injection surface and no apostrophe mangling. The CSV target guards differently, because a CSV field has no type to hide behind.

uppercase is withdrawn rather than invented. A spreadsheet font has no text transform, and capitalising the cell’s own text would turn presentation into data. The cell would stop round-tripping and would sort differently.

Padding and flow spacing are withdrawn on the same ground. A grid has no flow, and a cell has no box to inset.

Every cell takes 10 points unless something declares otherwise. Leaving it to the writer’s default would have this target and the PDF target supplying different numbers for one declaration.

The document model is deterministic, and the bytes aren’t yet. The target pins dates and meta is opt-in, but the current writer stamps archive entries with the packing time. Byte-identical output is a goal of the writer meant to replace it.

API walkthrough

Rendering

shell
npm install @quario/xlsx

Import the xlsx factory and render the same compiled report you render everywhere else. The result is a Uint8Array, and writing the file belongs to you. xlsx(options) takes only meta: { title, author, subject }, strings and never a date.

What lands where

One worksheet named Report, with the walk flattened onto it in render order. Report-header items, then per group instance its header items, nested content and footer items. Then the table’s header row, data rows and total rows, and last the report-footer items.

Grouping exists as that order alone. Group boundaries add no rows of their own.

Each band item takes one row with its cell in the first column, unmerged. A split takes one row, its slots the cells across it from the first column, with the width shares unread.

Keeping a number a number

A cell whose value is one bare interpolation writes its pre-stringify value as a native cell. A finite number stays a number, a boolean a boolean, a Date a date.

Every other cell joins to display text. That includes a lone null and anything a registered function already formatted. It’s the strongest practical reason to declare a format kind rather than format a value yourself.

A cell of more than one styled run lands as rich text, each run’s font merged over the cell’s. Bold, italic, size, face, and colour survive into the grid. A run holding one interpolation under a format presents its value there, so a sentence can carry a formatted amount without becoming a number cell. A run’s background isn’t read, because spreadsheet rich text carries fonts only.

typed and untyped, side by side
{
  "columns": [
    { "header": { "value": "Amount" }, "value": "{{ @.amount }}",
      "style": { "format": "currency" } },
    { "header": { "value": "Label" }, "value": "Total: {{ @.amount }}" }
  ]
}

The first column reaches the grid as a number under a currency number format. The second is text, because a literal run and a value can’t be one cell’s worth of number. Written as runs with the amount in a run of its own, that second cell stays text and presents the amount inside it.

Number formats

A declared kind becomes the cell’s number format. date builds its pattern from the form, and the reader’s own application spells the month and weekday names in its own language. The shape stays yours and the language becomes theirs.

The number kinds build theirs from the resolved fraction-digit count. An undeclared number is #,##0.00, percent is 0.00%, and currency is the code that cell wears over that currency’s minor units, so "USD"#,##0.00 but "JPY"#,##0.

Freezing the header

The first table’s header row anchors the frozen view, so everything from that row up stays visible while the body scrolls. It’s the spreadsheet analogue of the PDF target’s repeated headers. A report without a table freezes nothing.

The unlicensed marking

An unlicensed render writes the wording twice, as a styled banner in row 1 and as the workbook’s description property. The whole report therefore sits one row lower than a licensed render, frozen view included. That’s worth knowing if you are diffing output between the two states.

A grid that can still compute.

npm install quario
Getting started
© 2026 quario · KvK 61815977