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,
},
},
Footer note
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
| Technique | Purpose |
|---|---|
registerFontFamily | Registers both weights under a single family name so bold: true cells resolve automatically |
headerRows: 1 | Marks the first row as column headers for the line-item table |
headerColumns: 1 | Marks 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: 0 | Disables 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 |
borderColor | Sets the border line color, typically pulled from a shared palette constant for consistency across tables |
headerBackground | Applies a background fill to header rows/columns so they're visually distinguishable from data cells |
| Named styles | Centralise font size, color, and spacing so a rebrand touches one object |
tag: "Artifact" on footer | Excludes 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
h3should not appear without a precedingh2on the same logical section. tagged: trueandstructureRoot: "Document"are required at document creation.