Receipt template for JavaScript
A till receipt is 80 millimetres wide and as long as it needs to be. That sounds like a different problem from an A4 invoice, and it isn’t: this is the same kind of definition, rendered onto different paper. Nothing in the file below mentions 80 millimetres. The paper is the host’s to choose, so one definition prints to a roll, a sheet, or a screen.
The template
Everything the receipt says is in here, from the merchant block and the line table to the two VAT
bands and the card line. The document sets itself in mono at 7 point in a single declaration at
the top. That’s the way a receipt printer sets everything in one face. The table and footer below
inherit it without repeating it.
{
"data": "$.receipt.lines[*]",
"aggregates": {
"subtotal": "sum:[email protected] * @.unitPrice",
"vatLow": "sum:[email protected] == 9 ? @.qty * @.unitPrice * 0.09 : 0",
"vatHigh": "sum:[email protected] == 21 ? @.qty * @.unitPrice * 0.21 : 0"
},
"style": { "family": "mono", "size": 7 },
"header": [
{ "type": "text", "value": "{{ $.input.receipt.merchant.name }}",
"style": { "size": 11, "bold": true, "color": "#1e1f22", "align": "center" } },
{ "type": "text", "value": "{{ $.input.receipt.merchant.street }}",
"style": { "size": 7, "color": "#5e6062", "align": "center" } },
{ "type": "text", "value": "{{ $.input.receipt.merchant.city }}",
"style": { "size": 7, "color": "#5e6062", "align": "center" } },
{ "type": "text", "value": "VAT {{ $.input.receipt.merchant.vatId }}",
"style": { "size": 6, "color": "#989a97", "align": "center" } },
{
"type": "split",
"style": { "spaceBefore": 8, "spaceAfter": 8 },
"slots": [
{ "type": "text", "value": "{{ $.input.receipt.number }}", "width": 50,
"style": { "size": 7, "color": "#5e6062" } },
{ "type": "text", "value": [{ "value": "{{ $.input.receipt.issued }}" }, { "value": " {{ $.input.receipt.time }}" }],
"style": { "size": 7, "color": "#5e6062", "align": "right", "format": "date" } }
]
}
],
"detail": {
"header": {
"style": { "size": 6, "uppercase": true, "color": "#989a97",
"borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
},
"row": { "style": { "color": "#5e6062" } },
"columns": [
{ "header": "Item", "value": "{{ @.item }}", "width": 32 },
{ "header": { "value": "Qty", "style": { "align": "right" } }, "value": "{{ @.qty }}", "width": 12,
"style": { "align": "right" } },
{ "header": { "value": "Unit", "style": { "align": "right" } }, "value": "{{ @.unitPrice }}", "width": 20,
"style": { "align": "right", "format": "currency" } },
{ "header": { "value": "VAT", "style": { "align": "right" } }, "value": "{{ @.vatRate / 100 }}", "width": 13,
"style": { "align": "right", "color": "#989a97", "format": { "kind": "percent", "digits": 0 } } },
{ "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ round(@.qty * @.unitPrice, 2) }}", "width": 23,
"style": { "align": "right", "format": "currency" } }
],
"total": {
"style": { "color": "#989a97", "align": "right", "paddingTop": 0, "paddingBottom": 0 },
"rows": [
{
"style": { "color": "#5e6062", "paddingTop": 7 },
"cells": [
{ "value": "Subtotal", "span": 4 },
{ "value": "{{ round($.subtotal, 2) }}", "style": { "format": "currency" } }
]
},
{
"visible": "=$.vatLow != 0",
"cells": [
{ "value": "VAT 9%", "span": 4 },
{ "value": "{{ round($.vatLow, 2) }}", "style": { "format": "currency" } }
]
},
{
"visible": "=$.vatHigh != 0",
"cells": [
{ "value": "VAT 21%", "span": 4 },
{ "value": "{{ round($.vatHigh, 2) }}", "style": { "format": "currency" } }
]
},
{
"style": { "size": 9, "bold": true, "color": "#1e1f22" },
"cells": [
{ "value": "Total", "span": 4, "style": { "uppercase": true } },
{ "value": "{{ round($.subtotal + $.vatLow + $.vatHigh, 2) }}", "style": { "format": "currency" } }
]
}
]
}
},
"footer": [
{
"type": "split",
"style": { "spaceBefore": 8 },
"slots": [
{ "type": "text", "value": "{{ $.input.receipt.payment.method }} ••••{{ $.input.receipt.payment.last4 }}",
"width": 60, "style": { "color": "#5e6062" } },
{ "type": "text", "value": "{{ round($.subtotal + $.vatLow + $.vatHigh, 2) }}",
"style": { "color": "#5e6062", "align": "right", "format": "currency" } }
]
},
{ "type": "text", "value": "Auth {{ $.input.receipt.payment.auth }}",
"style": { "size": 6, "color": "#989a97" } },
{ "type": "text", "value": "Thank you",
"style": { "size": 8, "color": "#1e1f22", "align": "center", "spaceBefore": 8 } },
{ "type": "text", "value": "{{ $.input.receipt.note }}",
"style": { "size": 6, "color": "#989a97", "align": "center" } }
]
}One detail is worth copying. Every item in the report header states its own size. That band
carries a headline default — bold at 14 point — which sits above the document default. A header
item that declares no size comes out headline-sized. Items in the table and the footer have no such
default and simply inherit the 7 point set at the top.
Two things here that the invoice doesn’t do. The VAT bands are conditional
aggregates. "sum:[email protected] == 9 ? @.qty * @.unitPrice * 0.09 : 0" sums only the rows that
match. A receipt mixing 9% books with 21% printing totals each band separately, without grouping
or a second pass over the data. And each band declares "visible": "=$.vatLow != 0", so a receipt
with nothing in that band simply doesn’t print the line.
The total wraps its sum in round(…, 2). Subtotal plus the two bands computes to
83.75999999999999 for the sample basket — exact amounts, inexact sum. "format": "currency"
presents that as €83.76 on the roll. The workbook stores the number the cell holds, so the
definition rounds it first.
The data
This is the sample the preview renders. $.receipt.lines[*] selects the rows the table repeats
over. $.input reads the merchant, the number, and the payment:
{
"receipt": {
"number": "R-2026-0884",
"issued": "2026-08-21",
"time": "14:32",
"merchant": {
"name": "Quire Press Ltd.",
"street": "Binnenkant 42",
"city": "1011 BM Amsterdam",
"vatId": "NL8123.45.678.B01"
},
"lines": [
{ "item": "Marbled endpapers", "qty": 4, "unitPrice": 3.5, "vatRate": 21 },
{ "item": "Bookcloth, quarter", "qty": 1, "unitPrice": 18, "vatRate": 21 },
{ "item": "Proof copy, A4", "qty": 2, "unitPrice": 6, "vatRate": 21 },
{ "item": "Type specimen book", "qty": 1, "unitPrice": 28, "vatRate": 9 }
],
"payment": { "method": "Maestro", "last4": "4417", "auth": "0043821" },
"note": "Returns within 14 days with this receipt"
}
}The header line carries both as two styled runs. The date sits in a run of its own, which the
line’s "format": "date" presents as 21 Aug 2026. The time sits in the run beside it, printed
exactly as the data supplied it. That’s deliberate: a till knows the local time, and the receipt
just prints what it receives.
What it renders
This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders
them in your browser. That’s an 80 mm roll, because it’s what this page’s host asked for. The PDF
button hands you the same receipt as real @quario/pdf bytes, one page, 226.8 × 453.5 points. 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 receipt from './receipt.report.json' with { type: 'json' }
const q = quario({ locale: 'en-IE', currency: 'EUR' }) // what `format` presents through
const report = q.report(receipt)
const data = await getSale(id) // your data
const page = await report.render(html(), data) // ready to displayThe roll is a render option, not part of the document. size takes a [width, height] in points —
80 mm is 226.8 of them — and the margin comes down to what a receipt printer actually leaves:
import { writeFileSync } from 'node:fs'
import { pdf } from '@quario/pdf'
const bytes = await report.render(pdf({ page: { size: [226.8, 453.5], margin: 14 } }), data)
writeFileSync('receipt.pdf', bytes)Swap that one object for { size: 'A4', margin: 54 } and the same definition prints a full-sheet
receipt instead. See JSON to PDF for both PDF routes side by side.
Make it yours
The document says what belongs on the paper. The host says how big the paper is. So the edits you’d
expect to be layout work are edits to data. Change the roll width for a 58 mm printer. Add a logo
above the merchant name. Drop the VAT bands for a jurisdiction that doesn’t need them, and localise
the labels. All of it diffs in a pull request, and the same definition renders identically in Node,
the browser, and at the edge. It renders without eval, so a customer’s own line items never
become code. Security →
FAQ
Can I print this to a thermal receipt printer?
The PDF route gives you an 80 mm page you can send to any printer that takes one. For an ESC/POS printer that wants raw commands rather than a page, render the same definition through your own target. The engine hands a target a stream of bands, items, and resolved styles, not markup.
Can I generate the receipt as a PDF?
Yes — @quario/pdf renders this definition straight to PDF bytes, no browser anywhere. The page
geometry above is the only thing you change to move between a roll and a sheet.
Full example →
Can I export the receipt to Excel?
Yes — @quario/xlsx renders the same definition to a workbook, and it simply ignores the roll
geometry. A worksheet has no page to be 80 mm wide. Nothing to change if you do. The money
columns declare "format": "currency" rather than baking a formatted string into the cell, so
the amounts arrive as real numbers.
Full example →
Can I use this receipt template commercially?
The definition is yours — the report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Taking real money at a real counter is past evaluation, so that needs a seat for each developer working with it. Deployment is then unlimited and royalty-free. Pricing →
What if a sale is only one VAT rate?
Then one band prints and the other doesn’t — that’s what visible on each band is for. Adding a
third rate is one more aggregate and one more line. Nothing about the table changes.
Where are the other templates?
Browse the template gallery → — invoices, purchase orders, statements, and more.