How to Automate Generating PDF Documents With React and Typescript
One problem that many businesses will encounter at some point is generating PDFs—invoices, reports, receipts, or forms, to name some. Historically, this has been painful: Word and LaTeX don’t provide for much templating, and the alternatives are brittle HTML/CSS solutions or solutions like wkhtmltopdf or Weasyprint which only support a subset of CSS. But what if we could treat PDFs like React components? htmldocs is a new library that brings the great DX of the modern web to building documents, similar to what React Email did for email. What we’re going to build In this guide, we’ll learn to: Set up a React + TypeScript + TailwindCSS project Build an invoice document template Use htmldocs to preview and render it to a PDF Pass in dynamic props like customer details and line items Optionally publish your template to an API Tools we’ll use React and TypeScript for components and type-safety TailwindCSS for styling htmldocs for document rendering Project Setup If you’re starting fresh, the easiest way to scaffold a new project is npx htmldocs@latest init This will set up a ready-to-go project with some pre-built tepmlates. For this tutorial, we’ll assume we already have an existing project and want to add htmldocs to it. First, we’ll install the packages manually: npm install htmldocs @htmldocs/react @htmldocs/render Then, you want to create a documents folder for your templates. mkdir documents import { Document } from "@htmldocs/react"; import "~/index.css"; // import global TailwindCSS file function Invoice() { return ( Invoice starter ) }; export default Invoice; Inside the new folder, create a new file called Invoice.tsx: You should also add this folder to your tsconfig: // tsconfig.json { "include": [..., "documents"], } And also your tailwind.config.js: /** @type {import('tailwindcss').Config} */ module.exports = { //... other config options content: [..., "./documents/**/*.{js,jsx,ts,tsx}"], }; Finally, you want to add a new script to your package.json // package.json { ... "scripts": { "docs:dev": "npx htmldocs@latest dev" }, } Now, when you run npm run docs:dev, you should see a live preview server with your new document. Adding Dynamic Data In htmldocs, dynamic data is passed down as props to the component. Let’s modify our starter template to take in the customer details as well as an array of services. We'll also freshen up the layout and add some price calculations. import { Document, Head, Page, Footer } from "@htmldocs/react"; import "../src/styles/tailwind.css"; // import global TailwindCSS file interface Service { name: string; description?: string; quantity: number; rate: number; } interface InvoiceProps { customerName: string; customerEmail: string; services: Service[]; } function Invoice({ customerName, customerEmail, services }: InvoiceProps) { const subtotal = services.reduce( (acc, service) => acc + service.quantity * service.rate, 0 ); const taxRate = 0.1; // 10% tax const tax = subtotal * taxRate; const total = subtotal + tax; return ( Invoice for {customerName} INVOICE {new Date().toLocaleDateString()} {customerName} {customerEmail} Service Qty Rate Amount {services.map((service, index) => ( {service.name} {service.description && ( {service.description} )} {service.quantity} ${service.rate.toFixed(2)} ${(service.quantity * service.rate).toFixed(2)} ))} Subtotal: ${subtotal.toFixed(2)} Tax ({(taxRate * 100)}%): ${tax.toFixed(2)}

One problem that many businesses will encounter at some point is generating PDFs—invoices, reports, receipts, or forms, to name some. Historically, this has been painful: Word and LaTeX don’t provide for much templating, and the alternatives are brittle HTML/CSS solutions or solutions like wkhtmltopdf or Weasyprint which only support a subset of CSS.
But what if we could treat PDFs like React components? htmldocs is a new library that brings the great DX of the modern web to building documents, similar to what React Email did for email.
What we’re going to build
In this guide, we’ll learn to:
- Set up a React + TypeScript + TailwindCSS project
- Build an invoice document template
- Use htmldocs to preview and render it to a PDF
- Pass in dynamic props like customer details and line items
- Optionally publish your template to an API
Tools we’ll use
- React and TypeScript for components and type-safety
- TailwindCSS for styling
-
htmldocs
for document rendering
Project Setup
If you’re starting fresh, the easiest way to scaffold a new project is
npx htmldocs@latest init
This will set up a ready-to-go project with some pre-built tepmlates.
For this tutorial, we’ll assume we already have an existing project and want to add htmldocs
to it. First, we’ll install the packages manually:
npm install htmldocs @htmldocs/react @htmldocs/render
Then, you want to create a documents
folder for your templates.
mkdir documents
import { Document } from "@htmldocs/react";
import "~/index.css"; // import global TailwindCSS file
function Invoice() {
return (
<Document size="A4" orientation="portrait">
<h1 className="text-xl font-bold">Invoice starterh1>
Document>
)
};
export default Invoice;
Inside the new folder, create a new file called Invoice.tsx
:
You should also add this folder to your tsconfig:
// tsconfig.json
{
"include": [..., "documents"],
}
And also your tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
//... other config options
content: [..., "./documents/**/*.{js,jsx,ts,tsx}"],
};
Finally, you want to add a new script to your package.json
// package.json
{
...
"scripts": {
"docs:dev": "npx htmldocs@latest dev"
},
}
Now, when you run npm run docs:dev
, you should see a live preview server with your new document.
Adding Dynamic Data
In htmldocs, dynamic data is passed down as props to the component. Let’s modify our starter template to take in the customer details as well as an array of services. We'll also freshen up the layout and add some price calculations.
import { Document, Head, Page, Footer } from "@htmldocs/react";
import "../src/styles/tailwind.css"; // import global TailwindCSS file
interface Service {
name: string;
description?: string;
quantity: number;
rate: number;
}
interface InvoiceProps {
customerName: string;
customerEmail: string;
services: Service[];
}
function Invoice({ customerName, customerEmail, services }: InvoiceProps) {
const subtotal = services.reduce(
(acc, service) => acc + service.quantity * service.rate,
0
);
const taxRate = 0.1; // 10% tax
const tax = subtotal * taxRate;
const total = subtotal + tax;
return (
<Document size="A4" orientation="portrait">
<Head>
<title>Invoice for {customerName}title>
Head>
<Page className="p-8">
<div className="flex justify-between items-start mb-8">
<div>
<h1 className="text-2xl font-bold my-0">INVOICEh1>
<p className="text-gray-600">{new Date().toLocaleDateString()}p>
div>
<div className="text-right">
<p className="font-medium">{customerName}p>
<p className="text-gray-600">{customerEmail}p>
div>
div>
<table className="w-full mb-8">
<thead>
<tr className="border-b">
<th className="text-left py-2">Serviceth>
<th className="text-right py-2">Qtyth>
<th className="text-right py-2">Rateth>
<th className="text-right py-2">Amountth>
tr>
thead>
<tbody>
{services.map((service, index) => (
<tr key={index} className="border-b">
<td className="py-2">
<div>
<p className="font-medium">{service.name}p>
{service.description && (
<p className="text-sm text-gray-600">{service.description}p>
)}
div>
td>
<td className="text-right py-2">{service.quantity}td>
<td className="text-right py-2">${service.rate.toFixed(2)}td>
<td className="text-right py-2">
${(service.quantity * service.rate).toFixed(2)}
td>
tr>
))}
tbody>
table>
<div className="flex flex-col items-end gap-2">
<div className="flex justify-between w-48">
<span>Subtotal:span>
<span>${subtotal.toFixed(2)}span>
div>
<div className="flex justify-between w-48">
<span>Tax ({(taxRate * 100)}%):span>
<span>${tax.toFixed(2)}span>
div>
<div className="flex justify-between w-48 font-bold border-t pt-2">
<span>Total:span>
<span>${total.toFixed(2)}span>
div>
div>
<Footer
position="bottom-center"
className="text-center text-gray-600"
marginBoxStyles={{
marginBottom: "0.5in",
}}
>
{() => (
<p>Thank you for your business!p>
)}
Footer>
Page>
Document>
);
}
Invoice.PreviewProps = {
customerName: "Acme Corp",
customerEmail: "billing@acmecorp.com",
services: [
{ name: "Web Design", description: "Homepage redesign", quantity: 1, rate: 1500 },
{ name: "Consulting", description: "Technical architecture review", quantity: 2, rate: 800 },
],
};
export default Invoice;
Invoice.PreviewProps
helps set the default props on the document for rendering. It’s a required property to make sure that your document is able to compile properly.
Now your document should look like this:
To manually create a document based on this template, you can click on “Fill and Generate” which will pop up a form to fill out the details
Publishing to an API
htmldocs also provides a cloud service which you can publish documents to to put them behind a simple REST API.
Before publishing, you’ll need to add a document identifier to the file:
// rest of Invoice.tsx
Invoice.PreviewProps = {
customerName: "Acme Corp",
customerEmail: "billing@acmecorp.com",
services: [
{ name: "Web Design", description: "Homepage redesign", quantity: 1, rate: 1500 },
{ name: "Consulting", description: "Technical architecture review", quantity: 2, rate: 800 },
],
};
// add this line
Invoice.documentId = "invoice"
export default Invoice;
Then, you can run
npx htmldocs@latest login
npx htmldocs@latest publish documents/Invoice.tsx
Then, you’ll be able to log into the dashboard to see the uploaded document
You can now programmatically generate a PDF file by making a simple API call!
curl -X POST https://htmldocs.com/api/documents/invoice \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"props": {
"services": [
{
"name": "Web Design",
"rate": 1500,
"quantity": 1,
"description": "Homepage redesign"
},
{
"name": "Consulting",
"rate": 800,
"quantity": 2,
"description": "Technical architecture review"
}
],
"customerName": "Acme Corp",
"customerEmail": "billing@acmecorp.com"
},
"format": "json"
}'
And there we go!
To learn more about htmldocs, check out the:
- GitHub: https://github.com/htmldocs-js/htmldocs
- Website: htmldocs.com