Skip to main content

Cookbook: SaaS Invoice

Generate a branded SaaS invoice with a company logo, billing details, a line-item breakdown, and a summary table using renderTemplate().

Quick start

import { createDocument, rgb } from "@barrierbreak/a11ydocs-pdf";

async function ttf(url: string): Promise<Uint8Array> {
return new Uint8Array(await (await fetch(url)).arrayBuffer());
}

const cdn = "https://cdn.jsdelivr.net/npm/@expo-google-fonts";
const FONTS = {
inter400: `${cdn}/inter/400Regular/Inter_400Regular.ttf`,
inter700: `${cdn}/inter/700Bold/Inter_700Bold.ttf`,
};

const saasLogo = fetch("examples/assets/saas-logo.png");
const logo = await saasLogo.then((res) => res.arrayBuffer());

const doc = createDocument({
language: "en-US",
conformance: "pdfua-1",
tagged: true,
structureRoot: "Document",
title: "Invoice Template",
info: { author: "Lorem Ipsum" },
defaults: { fontSize: 12, color: rgb(0.15, 0.15, 0.2) },
});

const [inter400, inter700] = await Promise.all([
ttf(FONTS.inter400),
ttf(FONTS.inter700),
]);

doc.registerFontFamily("Inter", { regular: inter400, bold: inter700 });

Color palette

Define brand and muted tones up front so every block stays consistent:

const BRAND = rgb(0.05, 0.35, 0.75);
const MUTED = rgb(0.45, 0.45, 0.5);
const TABLE_BORDER = rgb(0.72, 0.76, 0.82);
const TABLE_HEADER = rgb(0.92, 0.94, 0.98);

rgb() accepts values in the 0–1 range. Equivalent hex helpers (hex()) work equally well if you prefer CSS-style values.

Building the invoice

Styles

Declare named styles in the template so heading levels and paragraph variants share a single source of truth:

styles: {
heading1: { fontSize: 26, color: BRAND, marginTop: 40 },
heading2: { fontSize: 14, color: BRAND, marginTop: 16, marginBottom: 0 },
heading3: { fontSize: 11, color: rgb(0.2, 0.2, 0.25) },
paragraph: { fontSize: 11, lineHeight: 17 },
muted: { fontSize: 10, color: MUTED, lineHeight: 15 },
label: { fontSize: 9, color: MUTED },
},

Logo block

Embed the company logo as a PNG at the top of the first page:

{
type: "png",
data: logo,
options: { altText: "Lorem Ipsum Logo", maxWidth: 100 },
},

Always supply altText so the document meets PDF/UA tagging requirements.

Billing details table

Use a two-column table to place Bill From and Bill To side by side. Mark the first row as headers with headerRows: 1:

{
type: "table",
rows: [
[
{ text: "Bill From:", header: true, bold: true },
{ text: "Bill To:", header: true, bold: true },
],
["Lorem Ipsum, New York, USA", "John Doe, Lorem Ipsum Team"],
],
options: {
headerRows: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

Line-item breakdown

The Bill Breakdown table uses both headerRows and headerColumns so screen readers can associate each data cell with its row and column headers:

 {
type: "table",
rows: [
[
{ text: "Description", header: true, bold: true },
{ text: "Quantity", header: true, bold: true },
{ text: "Unit Price", header: true, bold: true },
{ text: "Amount", header: true, bold: true },
],
[
{ text: "Software 1", header: true, scope: "row" },
{ text: "1" },
{ text: "$199.00" },
{ text: "$199.00" },
],
[
{ text: "Software 2", header: true, scope: "row" },
{ text: "4" },
{ text: "$29.00" },
{ text: "$116.00" },
],
[
{ text: "Software 3", header: true, scope: "row" },
{ text: "1" },
{ text: "$49.00" },
{ text: "$49.00" },
],
],
options: {
headerRows: 1,
headerColumns: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

Setting scope: "row" on the first cell of each data row lets assistive technology announce the product name when reading across columns.

Summary table

The Bill Summary uses headerRows: 0 because none of its rows are column headers — each row is a label/value pair instead:

  {
type: "table",
rows: [
[{ text: "Subtotal", header: true, bold: true }, { text: "$364.00" }],
[
{ text: "Discount Annual Loyalty (5%)", header: true, bold: true },
{ text: "-$18.20" },
],
[{ text: "Total Due", header: true, bold: true }, { text: "$345.80" }],
],
options: {
headerRows: 0,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

Tag the closing paragraph as "Artifact" on all pages except the last so it does not appear in the document's logical reading order mid-document:

footer: ({ pageNumber, totalPages }) => [
{
type: "paragraph",
text: "Generated by Lorem Ipsum",
options: {
fontSize: 10,
tag: pageNumber === totalPages ? "P" : "Artifact",
},
},
],

Full template

doc.renderTemplate({
autoOutline: { maxLevel: 6 },

page: {
size: "A4",
margin: 56,
font: "Inter",
spacing: {
paragraphAfter: 6,
headingBefore: { 2: 20, 3: 14 },
headingAfter: { 1: 6, 2: 8, 3: 6 },
tableBefore: 0,
tableAfter: 24,
},
},

styles: {
heading1: { fontSize: 26, color: BRAND, marginTop: 40 },
heading2: { fontSize: 14, color: BRAND, marginTop: 1 },
heading3: { fontSize: 11, color: rgb(0.2, 0.2, 0.25) },
paragraph: { fontSize: 11, lineHeight: 17 },
muted: { fontSize: 10, color: MUTED, lineHeight: 15 },
label: { fontSize: 9, color: MUTED },
},

blocks: [
{
type: "png",
data: logo,
options: { altText: "Lorem Ipsum Logo", maxWidth: 100 },
},

{ type: "heading", text: "Invoice #543482", options: { level: 1 } },

{ type: "heading", text: "Billing Details", options: { level: 2 } },
{ type: "paragraph", text: "Date: 01 June 2026" },
{
type: "table",
rows: [
[
{ text: "Bill From:", header: true, bold: true },
{ text: "Bill To:", header: true, bold: true },
],
["Lorem Ipsum, New York, USA", "John Doe, Lorem Ipsum Team"],
],
options: {
headerRows: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

{ type: "heading", text: "Bill Breakdown", options: { level: 2 } },
{
type: "table",
rows: [
[
{ text: "Description", header: true, bold: true },
{ text: "Qty", header: true, bold: true },
{ text: "Unit Price", header: true, bold: true },
{ text: "Amount", header: true, bold: true },
],
[
{ text: "Software 1", header: true, scope: "row" },
{ text: "1" },
{ text: "$199.00" },
{ text: "$199.00" },
],
[
{ text: "Software 2", header: true, scope: "row" },
{ text: "4" },
{ text: "$29.00" },
{ text: "$116.00" },
],
[
{ text: "Software 3", header: true, scope: "row" },
{ text: "1" },
{ text: "$49.00" },
{ text: "$49.00" },
],
],
options: {
headerRows: 1,
headerColumns: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

{ type: "heading", text: "Bill Summary", options: { level: 2 } },
{
type: "table",
rows: [
[{ text: "Subtotal", header: true, bold: true }, { text: "$364.00" }],
[
{ text: "Discount Annual Loyalty (5%)", header: true, bold: true },
{ text: "-$18.20" },
],
[{ text: "Total Due", header: true, bold: true }, { text: "$345.80" }],
],
options: {
headerRows: 0,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

{
type: "paragraph",
text: "Thank you for using Lorem Ipsum. This is a computer-generated invoice and does not require a signature.",
class: "muted",
options: { marginTop: 30 },
},
],

footer: ({ pageNumber, totalPages }) => [
{
type: "paragraph",
text: "Generated by Lorem Ipsum",
options: {
fontSize: 10,
tag: pageNumber === totalPages ? "P" : "Artifact",
},
},
],

pageNumber: { region: "footer", align: "right" },
});

export default doc;

Key techniques

TechniquePurpose
registerFontFamilyRegisters both weights under a single family name so bold: true cells resolve automatically
headerRows: 1Marks the first row as column headers for the line-item table
headerColumns: 1Marks the first column as row headers, enabling row/column association for screen readers
scope: "row"Explicitly scopes a header cell to its row in the breakdown table
headerRows: 0Disables row-header treatment for the summary's label/value pairs
borders: "all"Draws borders around every cell rather than just the table's outer edge
borderColorSets the border line color, typically pulled from a shared palette constant for consistency across tables
headerBackgroundApplies a background fill to header rows/columns so they're visually distinguishable from data cells
Named stylesCentralise font size, color, and spacing so a rebrand touches one object
tag: "Artifact" on footerExcludes repeated footer text from the logical reading order on non-final pages
autoOutline: { maxLevel: 6 }Builds a PDF bookmark tree from all heading levels automatically

PDF/UA conformance

This template targets conformance: "pdfua-1". To keep the document valid:

  • Every image block must include a non-empty altText.
  • Decorative elements such as page footers on intermediate pages should use tag: "Artifact".
  • Heading levels must not skip — an h3 should not appear without a preceding h2 on the same logical section.
  • tagged: true and structureRoot: "Document" are required at document creation.