Escape

The DOCX target

Intent

@quario/docx resolves the same definition every other target reads into the bytes of a .docx. Real Word tables, inline pictures, and a grouping that becomes the navigation pane. It’s a document the reader edits in their own application, rather than a page you have already drawn for them.

Design decisions

It’s a flow target, not a painter. It states the geometry and the typography and lets Word paginate, so it runs no layout of its own. The promise is the same geometry and typography. Where the pages break is the reader’s. That’s the deliberate difference from the PDF target, which paginates itself, and can therefore promise where every line falls. Choose by which promise you need: a document someone will edit, or a page that must fall the same way everywhere. Painters, flow targets, and a fragment describes the family.

A table detail is a real Word table. Declared column width percentages are exact. The columns that declared none divide the remainder evenly, not from their content: a flow target measures nothing. That’s the one approximation here. A fixed layout stops a reader re-measuring and losing the shares. The header row repeats after every page break, and a span becomes a gridSpan across the columns it covers.

A run that links is a Word hyperlink. A URL becomes a relationship of the part the run lands in, marked as leaving the package. One relationship serves every run pointing at the same place. An href beginning # becomes an anchor, and the first instance that declares that label writes the bookmark it names. That holds when the link comes before the group or sits in a page band. A link to a label no instance declares keeps its text and loses its link, so Word never shows a link that goes nowhere. A run whose text is empty writes no link.

Grouping becomes the outline. A group header’s first item takes a heading style by depth, so the navigation pane mirrors the report’s own structure. Those heading styles exist for the outline rather than the look: Normal is empty. The report default and every authored declaration land as direct formatting on the runs and paragraphs that wear them. A look that relied on a style lookup is a look three readers may resolve three different ways.

There is no stylesheet and no font option. A document is final when it opens, so what this target supplies where the author declared nothing, it supplies itself. The report header is the one size it derives rather than fixes: 1.4× the report default, rounded to Word’s half-points. A document declaring "size": 20 then renders its headline at 28 rather than a fixed 14.

A collapsed group writes its header and its footer. Word has nowhere to put a row a reader could open, so it omits the rows between them. Nothing touches the aggregates, so the footer still totals them.

API walkthrough

Rendering the bytes

report.ts
import { quario } from 'quario'
import { docx } from '@quario/docx'

const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)

const bytes = await report.render(
  docx({ page: { size: 'A4' }, meta: { title: 'Sales 2026' } }),
  data,
)

page is the PDF target’s, validated identically. size defaults to 'A4', an array is a custom [width, height] in points, and margin defaults to 54 points when the document declares none. A document’s own page.size, page.orientation and page.margin write the section’s pgSz, w:orient and per-side pgMar, and declaring the same key in both places is a render error. meta writes document properties, and takes strings only: no date, so no clock reaches the package.

Page bands, and live fields

Page bands become a Word section’s header and footer parts. This target calls each band’s closure twice: once at page 1 of 2, and once at page 2 of 2. Equal results become one part. Results that differ become a first-page part and a default one, so the cover-page idioms visible: "=page.number > 1" and "== 1" resolve exactly.

A bare {{ page.number }} or {{ page.total }} becomes a live field the reader’s own application recomputes. Anything computed from them freezes at the value the probe read. A band gated on page.total therefore sees 2 — best-effort, exactly as the PDF target’s height probe is.

Breaking and restarting

A group’s break becomes a page break on the instance’s first paragraph for a leading edge. A trailing edge puts one on the paragraph after its last. A table carries no paragraph properties, so a break owed where a table starts now gets a paragraph of its own to sit on. Without that carrier it would travel to the next paragraph and land on the wrong page, or on none. reset: "page" opens a new section whose page numbering restarts at 1, and needs break to be "before" or "around".

Determinism

The same schema, data and options produce byte-identical output on every supported runtime. Every zip entry carries the same 1980-01-01 stamp, no part carries a clock, and the document properties have no slot for one.