Human-readable rendering
renderHtml(invoice, { lang?, locale?, standalone? }), renderPdf(invoice, { lang?, locale? }) and the React component <Invoice> turn a canonical invoice into something a person can read, print or archive. All three draw from one view model (buildInvoiceView in src/render/model.ts), so they show the same things: a header with the document type, number, key dates and the amount due; seller and buyer with postal addresses and labelled identifiers; references; the lines; allowances and charges; the VAT breakdown; the totals; payment instructions; notes; and attachments listed by name.
import { convert, renderHtml, renderPdf } from 'eu-einvoice-canonical';
const html = renderHtml(invoice, { lang: 'de' }); // a complete HTML document (string)const fragment = renderHtml(invoice, { standalone: false }); // just <style> + <article>, to embedconst pdf = await renderPdf(invoice, { lang: 'fr' }); // Uint8Array, A4einvoice render --as html invoice.xml > invoice.htmleinvoice render --as pdf invoice.xml --out invoice.pdf --lang pleinvoice render --as pdf a.xml b.xml --out out/ # one <invoice-id>.pdf per invoiceFormatting. Dates, times, amounts, quantities and percentages default to the seller’s address country. When that country is missing or unknown, a source format associated with exactly one country supplies the default; generic or multi-country formats fall back to lang (English by default). Country locales use CLDR’s likely subtags. lang selects labels independently; locale overrides value formatting, for example renderHtml(invoice, { lang: 'en', locale: 'ro-RO' }), <Invoice invoice={invoice} lang="en" locale="ro-RO" />, or einvoice render --as html --locale ro-RO invoice.xml. To retain the previous language-based formatting, set locale to the same value as lang.
-
Amounts, quantities and percentages use the locale’s separators and patterns (
€1,234.50,1.234,50 €,12,5 %). Intl supplies only the symbols and the pattern; the digits are copied from the invoice’s exact decimal string, so there is no floating-point rounding and extra decimals are kept (€0.125). Amounts are padded to the currency’s minor unit, never cut. -
Dates are shown in the formatting locale (
15.10.2026,Oct 15, 2026) inside<time datetime>, with the ISO date as a tooltip. Issue times use the locale’s 12/24-hour clock without shifting the source wall-clock time; the original timestamp remains in the tooltip. -
Postal addresses follow each address’s own country, even on cross-border invoices: postcode before city by default; separate city, county and postcode lines for GB/IE; city, region and postcode together for US/CA/AU; postcode, city and province together for Italy; city followed by postcode for Latvia; and city before street with postcode last for Hungary. Unknown countries keep the default layout and all supplied components. Country names use the label language. The layouts follow Poste Italiane, USPS and UPU country sheets for Hungary, Latvia and Ireland; street text and postal codes are preserved.
-
IBANs (with a valid check sum) are grouped in fours; VAT IDs, company registrations, GLNs, Leitweg-IDs and other identifiers carry a label naming their scheme.
-
Quantities show their unit (UN/ECE Rec 20:
10 pcs,2 hr,10 Std.,2,5 m²), with the code as a tooltip; a price for more than one unit says so (per 100 pcs). -
VAT categories and payment means are named (
Standard rate,Reverse charge,SEPA credit transfer), with the code as a tooltip. -
Notes, descriptions and payment terms retain their wording and language. Blank lines split paragraphs, and runs of lines starting with
-,*,•or1.become bullet lists. Dates, numbers and currency mentions embedded in prose are preserved rather than guessed or translated. -
Credit notes carry a “Credit note” badge, a red accent and “Amount credited” in place of “Amount due”.
-
Languages: labels exist for
en,de,fr,it,es,nl,plandro; any other tag keeps English labels and is used for thelangattribute. Value formatting follows country or the explicitlocaleoverride independently. -
HTML is self-contained: inline CSS, print styles (the standalone page prints on A4 with 15 mm margins), no scripts, no links, no images and no external resources, and the standalone page carries a Content-Security-Policy (
default-src 'none') on top. It uses landmarks, labelled sections and<th scope>so screen readers can navigate it. Invoices are untrusted input: every value is escaped (ampersand, angle brackets, both quote characters and backtick; control characters dropped), attachments are listed by name only (never decoded or linked) and external URIs appear as plain text.test/render.test.tsinjects<script>and attribute-breaking strings into every text field and checks that only the renderer’s own tags and attributes come out. -
PDF is built with pdf-lib and the standard Helvetica fonts (nothing embedded), with amounts written with the currency code. Text wraps, long line lists continue on further pages with the table header repeated, and each page says “Page x of y”. Output is deterministic (the PDF dates come from the invoice issue date). The standard fonts only cover the WinAnsi (Western European) character set: accented letters outside it lose the accent (
șass,łasl), and other characters, such as Cyrillic, Greek or CJK, are drawn as?in the PDF but are intact in the HTML. -
The browser converter shows the Invoice tab with the React component below.
React component
Section titled “React component”eu-einvoice-canonical/react exports <Invoice>, which renders the same markup as renderHtml (a test compares the two, element for element, across several formats and languages):
import { Invoice } from 'eu-einvoice-canonical/react';import 'eu-einvoice-canonical/react/invoice.css';
export function InvoicePage({ invoice }) { return <Invoice invoice={invoice} lang="de" className="my-invoice" />;}The result is an A4-like sheet: the title and document number on the left with the issue date, due date and a large amount due on the right, under a coloured rule; seller, buyer and delivery side by side; then references, the lines table, allowances and charges, the VAT breakdown, the totals with subtotals ruled off and the amount due emphasised, and the payment instructions with the IBAN in a monospace face.
-
Parts. Build your own layout from
InvoiceHeader,InvoiceParties,InvoiceReferences,InvoiceLines,InvoiceAdjustments,VatBreakdown,InvoiceTotals,PaymentDetailsandInvoiceNotes. Each takesinvoice,langand an optionallocale, or aviewfrombuildInvoiceView(invoice, lang)to format once and share:const view = buildInvoiceView(invoice, 'fr');<><InvoiceHeader view={view} /><InvoiceLines view={view} /><InvoiceTotals view={view} className="right" /></> -
Styling.
invoice.cssis plain CSS with one class per rule and no dependencies. Theme it with custom properties on any ancestor:--einvoice-font,--einvoice-mono,--einvoice-size,--einvoice-ink,--einvoice-muted,--einvoice-rule,--einvoice-surface,--einvoice-subtle,--einvoice-accent,--einvoice-credit,--einvoice-radius.classNameis added to the outermost element;unstyledleaves out every built-in class so the stylesheet does not apply and you can style the semantic markup yourself. The stylesheet is generated from the onerenderHtmlinlines (bun run build:react-css). -
shadcn/ui. Add the class
einvoice-shadcnto the invoice (or any ancestor) and the variables come from your shadcn theme:--foreground,--muted-foreground,--border,--background,--muted,--primary,--destructive,--radius,--font-sans,--font-mono, so it follows your colours and dark mode. Neither shadcn nor Tailwind is a dependency. The mapping expects complete colour tokens (shadcn on Tailwind v4); with the older HSL-triplet tokens, set the--einvoice-*variables yourself, e.g.--einvoice-ink: hsl(var(--foreground)).<Invoice invoice={invoice} lang="de" className="einvoice-shadcn" /> -
Safe by construction. Text reaches the DOM only as React children and attribute values, which React escapes; the components never use
dangerouslySetInnerHTML, andtest/react.test.tsxrenders an invoice with<script>and attribute-breaking strings in every field. No effects and no browser globals, so it renders on the server (renderToString, React Server Components) as well as in the browser. Element ids come fromuseId, or fromidPrefixwhen you need stable ones. -
Dependencies.
reactandreact-dom(18 or later) are optional peer dependencies; there are no others. Only the/reactentry imports React: the main entry and the HTTP server never do, which a test checks by walking their import graphs. -
Demo.
bun examples/react/render.tsx invoice.xml de > invoice.htmlserver-renders any supported file with the component into a standalone page.