Word documents, in JavaScript
You need a .docx — something a colleague opens, reads, and edits. The thing to
settle before you start is who decides where the pages break, because for Word
it isn’t you. quario states the geometry and the typography; Word does the
paginating when the file opens.
The approach in one line
Your data + a report definition → a .docx. No Word install on the server,
no headless browser, no template file to fill in. The same definition that
renders a PDF renders the Word file.
Do it with quario
Install:
npm install @quario/docxDescribe the report once. Two declarations here do work that only Word cashes
in: groups becomes the navigation pane, and the bare {{ page.number }} in
page.footer becomes a field Word recomputes for itself.
{
"data": "$.entries[*]",
"aggregates": { "total": "sum:[email protected]" },
"header": [
{ "type": "text",
"value": "Quarterly review",
"style": { "size": 20, "bold": true, "spaceAfter": 10 } }
],
"groups": [
{ "name": "team",
"by": "[email protected]",
"aggregates": { "subtotal": "sum:[email protected]" },
"header": [
{ "type": "text",
"value": "{{ team.key }}",
"style": { "size": 13, "bold": true, "spaceBefore": 14, "spaceAfter": 4 } }
] }
],
"detail": {
"columns": [
{ "header": "Item", "value": "{{ @.item }}", "width": 70 },
{ "header": { "value": "Amount", "style": { "align": "right" } },
"value": "{{ @.amount }}",
"style": { "align": "right", "format": "currency" }, "width": 30 }
],
"total": {
"style": { "align": "right", "bold": true },
"rows": [
{ "cells": [
{ "value": "Subtotal {{ team.key }}" },
{ "value": "{{ round(team.subtotal, 2) }}", "style": { "format": "currency" } }
] }
]
}
},
"page": {
"footer": [
{ "type": "text", "value": "Page {{ page.number }} of {{ page.total }}", "style": { "size": 8 } }
]
}
}Render it:
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { docx } from '@quario/docx'
import definition from './review.report.json' with { type: 'json' }
const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)
const bytes = await report.render(
docx({ page: { size: 'A4' }, meta: { title: 'Quarterly review' } }),
data,
)
writeFileSync('review.docx', bytes)Word paginates, not quario
@quario/docx is a flow target. It runs no layout of its own: it states the
geometry and the typography, and the reader’s application decides where page two
starts. The promise is the same look, not the same page breaks.
That’s the trade against the PDF target, which paints its own pages and can therefore promise where every line falls. Pick by which promise you need. A document someone will edit wants Word. A document that must look identical on every desk wants the PDF.
What Word gives you back
A navigation outline. Each group header’s first item takes a heading style by depth, so the grouping you already declared becomes the navigation pane. Nothing extra to declare.
Live page numbers. A bare {{ page.number }} or {{ page.total }} becomes a
PAGE or NUMPAGES field. Delete a paragraph in Word and the numbering
corrects itself, because Word owns it now. Anything you compute from those
tokens freezes at render instead, which is the one place the flow model shows
through.
Real tables. A detail is a Word table, not a picture of one. Declared
column widths are exact, the header row repeats across page breaks, and a reader
can put their cursor in a cell and type.
You may not need to write a definition
Every template on this site is a definition, and a definition is target-blind. The invoice, the statement, the payslip, and the rest all render to Word through the same call as above — nothing about them is PDF-specific. If you came looking for a Word template, start there and change the parts that are yours.
FAQ
Does the server need Word installed?
No. @quario/docx writes the OOXML package itself, and its only runtime
dependency is a zip writer. There’s no Word, no LibreOffice, and no headless
browser anywhere in the path.
Can I control where the pages break?
Only coarsely, and on purpose. A group’s break: "page" starts an instance on a
fresh page, and reset: "page" restarts the numbering. Beyond that the breaks
are Word’s. If you need them to be yours, render
the same definition to PDF.
Are the bytes deterministic?
Yes. The same schema, data and options produce byte-identical output on every supported runtime. Every zip entry carries the same fixed stamp, and no part of the package holds a clock.
How big is it?
The engine is under 5 kB minified and compressed, and the DOCX target adds its own on top, plus a zip writer.
Can I use it for free?
Evaluating it’s free and has no time limit — the full engine, no key to request — and evaluation output carries a watermark. Anything past evaluating it, including shipping it in an application or running it in your business, needs a seat per developer. Deployment is then unlimited and royalty-free, however many servers or users you have. See pricing →