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
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.