Invoice template for JavaScript
A production invoice is fiddly. Line items wrap, totals must reconcile, currency and dates have to be right, and the layout has to print cleanly to A4. This is a ready-made quario invoice report — copy it, point it at your data, and render to HTML, PDF, Excel, or Word. No layout engine to wire up, no PDF coordinates to nudge.
The template
The whole invoice is a definition — data you can version and review. Addresses, VAT numbers, dates, the totals block and the payment line are all in here. Nothing about this document lives in code. This is the exact file the preview below renders:
{
"data": "$.invoice.lines[*]",
"aggregates": {
"subtotal": "sum:[email protected] * @.unitPrice",
"vat": "sum:[email protected] * @.unitPrice * @.vatRate / 100"
},
"style": { "family": "sans", "size": 10 },
"header": [
{
"type": "split",
"slots": [
{ "type": "text", "value": "Invoice No. {{ $.input.invoice.number }}", "width": 60,
"style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
{ "type": "text", "value": [{ "value": "Issued " }, { "value": "{{ $.input.invoice.issued }}" }],
"style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
]
},
{
"type": "split",
"style": { "spaceAfter": 4 },
"slots": [
{ "type": "text", "value": "{{ $.input.invoice.reference }}", "width": 60,
"style": { "size": 8, "color": "#989a97" } },
{ "type": "text", "value": [{ "value": "Due " }, { "value": "{{ $.input.invoice.due }}" }],
"style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
]
},
{
"type": "split",
"style": { "spaceBefore": 34 },
"slots": [
{ "type": "text", "value": "From", "width": 50,
"style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
{ "type": "text", "value": "Bill to",
"style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } }
]
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "{{ $.input.invoice.seller.name }}", "width": 50,
"style": { "bold": true, "color": "#1e1f22" } },
{ "type": "text", "value": "{{ $.input.invoice.customer.name }}",
"style": { "bold": true, "color": "#1e1f22" } }
]
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "{{ $.input.invoice.seller.street }}", "width": 50,
"style": { "color": "#5e6062" } },
{ "type": "text", "value": "{{ $.input.invoice.customer.street }}",
"style": { "color": "#5e6062" } }
]
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "{{ $.input.invoice.seller.city }}", "width": 50,
"style": { "color": "#5e6062" } },
{ "type": "text", "value": "{{ $.input.invoice.customer.city }}",
"style": { "color": "#5e6062" } }
]
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "{{ $.input.invoice.seller.country }}", "width": 50,
"style": { "color": "#5e6062" } },
{ "type": "text", "value": "{{ $.input.invoice.customer.country }}",
"style": { "color": "#5e6062" } }
]
},
{
"type": "split",
"style": { "spaceAfter": 40 },
"slots": [
{ "type": "text", "value": "VAT {{ $.input.invoice.seller.vatId }}", "width": 50,
"style": { "size": 8, "color": "#989a97" } },
{ "type": "text", "value": "VAT {{ $.input.invoice.customer.vatId }}",
"style": { "size": 8, "color": "#989a97" } }
]
}
],
"detail": {
"header": {
"style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
"borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
},
"row": { "style": { "color": "#5e6062" } },
"columns": [
{ "header": "Item", "value": "{{ @.item }}", "width": 46 },
{ "header": { "value": "Qty", "style": { "align": "right" } }, "value": "{{ @.qty }}", "width": 10,
"style": { "align": "right" } },
{ "header": { "value": "Unit", "style": { "align": "right" } }, "value": "{{ @.unitPrice }}", "width": 16,
"style": { "align": "right", "format": "currency" } },
{ "header": { "value": "VAT", "style": { "align": "right" } }, "value": "{{ @.vatRate / 100 }}", "width": 10,
"style": { "align": "right", "color": "#989a97", "format": { "kind": "percent", "digits": 0 } } },
{ "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ round(@.qty * @.unitPrice, 2) }}", "width": 18,
"style": { "align": "right", "format": "currency" } }
],
"total": {
"style": { "color": "#5e6062", "align": "right", "paddingTop": 0, "paddingBottom": 0 },
"rows": [
{
"cells": [
{ "value": "Subtotal", "span": 4 },
{ "value": "{{ round($.subtotal, 2) }}", "style": { "format": "currency" } }
]
},
{
"cells": [
{ "value": "VAT 21%", "span": 4 },
{ "value": "{{ round($.vat, 2) }}", "style": { "format": "currency" } }
]
},
{
"style": { "bold": true, "color": "#1e1f22", "paddingTop": 5,
"borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
"cells": [
{ "value": "Total due", "span": 4 },
{ "value": "{{ round($.subtotal + $.vat, 2) }}", "style": { "format": "currency" } }
]
}
]
}
},
"page": {
"footer": [
{
"type": "text",
"value": "{{ $.input.invoice.terms }} to {{ $.input.invoice.seller.iban }}, quoting {{ $.input.invoice.number }}.",
"style": { "size": 8, "color": "#989a97", "paddingTop": 10,
"borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" }
}
]
}
}Four pieces of syntax carry the whole thing. data is a JSONPath that selects the rows the table
repeats over. {{ … }} interpolates a value — @ is the current line, $ is the report root, and
$.input is the data you passed in. "sum:[email protected] * @.unitPrice" declares an aggregate, which lands
on $ for the totals to read. And a split places its slots across the line instead of stacking
them, which is how the two addresses sit side by side.
The table says each shared declaration once. The mono column labels and the rule under them are
detail.header‘s own style, and the body rows’ colour is detail.row’s. A column states only
what it alone wants: its alignment, its format. A row’s box reaches its cells on every target, so
the rule under the labels is one declaration rather than one per column.
The totals are detail.total, a block of rows in the table itself rather than a block beneath it.
Every figure lands in the column it belongs to instead of in widths you match by eye. Each label is
one cell with a span covering the columns it reaches. A total row is a label and an amount, not a
line of empty spacer cells. What the rows share — the colour, the right alignment, the tighter
pitch — is the block’s own style, said once. A row states only what differs. The rule above
Total due is a borderTop declared on that row alone, drawn continuously across the width.
The payment line sits in page.footer, not the report footer. It’s drawn at the foot of every page
rather than wherever the totals happen to end. That’s what lets a one-page invoice look like a
page. Content sits at the top, the terms on the baseline, and the white space between them on
purpose. Spacing between the blocks above is spaceBefore / spaceAfter in points, declared on
the split that opens each one.
A definition declares amounts rather than formatting them. The cell interpolates the number and
"format": "currency" says how to show it, from the locale and currency you hand quario(). Every
sum a cell does itself wraps in round(…, 2) — a line’s qty × unitPrice, and subtotal plus VAT.
Exact amounts can add up to an inexact float, and where a page presents the number the workbook
stores it.
Dates work the same way. The Issued and Due lines use styled runs — a value written as a
list, [{ "value": "Issued " }, { "value": "{{ … }}" }]. The date stands in a run of its own. The
line’s "format": "date" then presents it in the instance locale’s medium form, 14 Aug 2026
under en-IE, and leaves the label alone. A format needs one value to speak about, which is
what the split gives it. The same line as a single string, "Issued {{ … }}", would render the
ISO date and plan() would warn. The template registers no function for any of this. A form of
short, long or full picks another shape when a document wants one.
The data
A definition renders nothing on its own — you hand it a document. This is the sample the preview
below uses. $.invoice.lines[*] in the definition selects its rows, and $.input reads the rest:
{
"invoice": {
"number": "2026-1042",
"issued": "2026-08-14",
"due": "2026-09-13",
"reference": "PO 88231",
"seller": {
"name": "Quire Press Ltd.",
"street": "Binnenkant 42",
"city": "1011 BM Amsterdam",
"country": "Netherlands",
"vatId": "NL8123.45.678.B01",
"iban": "NL91 ABNA 0417 1643 00"
},
"customer": {
"name": "Folio & Sons",
"street": "12 Marlborough Street",
"city": "Dublin 1",
"country": "Ireland",
"vatId": "IE6388047V"
},
"lines": [
{ "item": "Typesetting", "qty": 12, "unitPrice": 120, "vatRate": 21 },
{ "item": "Folding & gathering", "qty": 4, "unitPrice": 80, "vatRate": 21 },
{ "item": "Case binding, quarter cloth", "qty": 250, "unitPrice": 6.4, "vatRate": 21 },
{ "item": "Foil stamping, spine", "qty": 250, "unitPrice": 1.1, "vatRate": 21 },
{ "item": "Proof copies, courier", "qty": 1, "unitPrice": 48, "vatRate": 21 }
],
"terms": "Payment within 30 days"
}
}What it renders
This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders
them in your browser, right here. The PDF button hands you the same invoice as real @quario/pdf
bytes, no browser rendering involved. The definition asks for sans, serif and mono. The faces
you see are this page’s own, handed to the viewer and the PDF export as the same TrueType files.
Since @quario/viewer 0.4.0 the sheet is the layout’s own pages painted on a canvas. The screen
measures and draws in exactly what the PDF embeds. The DOCX button beside it hands the same
definition to @quario/docx, where Word paginates and resolves the faces itself:
Use it
npm install quario @quario/htmlimport { quario } from 'quario'
import { html } from '@quario/html'
import invoice from './invoice.report.json' with { type: 'json' }
const q = quario({ locale: 'en-IE', currency: 'EUR' }) // what `format` presents through
const report = q.report(invoice)
const data = await getInvoice(id) // your data
const page = await report.render(html(), data) // ready to display or printShow page in the browser for a live preview, or render the same compiled report straight to a
PDF for sending and archiving — no browser involved:
import { writeFileSync } from 'node:fs'
import { pdf } from '@quario/pdf'
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)
writeFileSync('invoice.pdf', bytes)See JSON to PDF for both PDF routes side by side.
Make it yours
Because the invoice is declarative, changing it means changing data. Drop the VAT column for a zero-rated customer, swap in your logo, reorder columns, localise the labels. They’re all edits to the definition, all diff-able in a pull request. The same file renders identically in Node, the browser, and at the edge, so your preview and your PDF never drift apart. And it renders without eval, so building invoices from customer data doesn’t hand you an injection problem. Security
→
FAQ
Can I generate the invoice as a PDF?
Yes — @quario/pdf renders the same definition straight to PDF bytes, with no browser anywhere. If you’d rather go through HTML and your own print CSS, that route works too. Full example →
Can I export the invoice to Excel?
Yes — @quario/xlsx renders the same definition to a workbook, with nothing to change. The amount columns declare "format": "currency" rather than baking a formatted string into the cell. The workbook gets a real number carrying a currency number format ("EUR"#,##0.00). The column reads as currency and your recipient can still sum it. Full example →
Can I use this invoice template commercially?
The definition is yours — the report definitions, data, and output you create with quario belong to you. quario itself is free to evaluate without a time limit. Invoicing real customers is past evaluation, so that needs a seat for each developer working with it. Deployment is then unlimited and royalty-free. Pricing →
Does it handle multiple currencies / tax lines?
This one already carries a VAT rate per line and totals it into its own row, all from the definition. A second rate is another field in your data and another aggregate. Currency is the one to think about. "format": "currency" presents every amount in the currency you gave quario(), which is right for an invoice denominated in one currency. For a document mixing several, a cell names its own — "currency": "[email protected]" beside the format — and the amount stays a number. That row’s cell reads US$ on the page and carries "USD"#,##0.00 in the workbook rather than arriving as text.
Where are the other templates?
Browse the template gallery → — quotes, receipts, purchase orders, and more.