Write PDF templates in React + TypeScript during development. Compile to Handlebars + HTML at build time. Generate PDFs with Puppeteer at runtime.
- Great DX: Write templates with React, TypeScript, Tailwind — autocomplete, type checking, component reuse
- Fast in production: Zero React runtime — compiled templates are pure HTML + Handlebars
- Lightweight: Only Handlebars + puppeteer-core in production, no heavy React bundle
- Flexible: Works in any Node.js environment with Chromium available
npm install react-template-pdf// templates/invoice.tsx
import { useTemplate, PageBreak, KeepTogether } from 'react-template-pdf'
type InvoiceData = {
clientName: string
total: number
items: { name: string; price: number }[]
hasDiscount: boolean
}
export function transform(data: InvoiceData) {
return {
...data,
formattedTotal: `$${data.total.toFixed(2)}`,
}
}
export default function Invoice(props: ReturnType<typeof transform>) {
const $ = useTemplate(props)
return (
<div className="p-8">
<h1>Invoice for {$.clientName}</h1>
<table>
{$.items.$each((item) => (
<tr>
<td>{item.name}</td>
<td>{item.price}</td>
</tr>
))}
</table>
{$.hasDiscount.$if(
() => <p>Discount applied!</p>,
() => <p>No discount</p>,
)}
<p>Total: {$.formattedTotal}</p>
</div>
)
}npx rtpdf build ./templatesimport { PdfEngine } from 'react-template-pdf/runtime'
const engine = new PdfEngine({
chromiumPath: '/usr/bin/chromium',
poolSize: 4,
})
await engine.start()
const pdf = await engine.generate('./dist/templates/invoice', {
clientName: 'John Doe',
total: 299.99,
items: [
{ name: 'Widget', price: 199.99 },
{ name: 'Gadget', price: 100.00 },
],
hasDiscount: true,
})
// pdf is a Buffer — save, send, stream, etc.const $ = useTemplate(props)
// Simple values
{$.fieldName}
// Loops
{$.items.$each((item) => (
<tr><td>{item.name}</td></tr>
))}
// Conditionals
{$.isActive.$if(
() => <p>Active</p>,
() => <p>Inactive</p>,
)}Export a transform function to process payload data before template rendering:
export function transform(data: RawInput) {
return {
...data,
formattedDate: new Date(data.date).toLocaleDateString(),
total: data.items.reduce((sum, i) => sum + i.price, 0),
}
}Transforms run at runtime with real data — use them for formatting, calculations, and derived fields.
| Component | Description |
|---|---|
<PageBreak /> |
Force a page break |
<KeepTogether> |
Prevent page break inside a block |
asset(path) |
Embed static images as base64 |
The package includes automatic pagination CSS that prevents awkward page breaks:
- Table rows never break in the middle
- Headings stay with their content
- Figures stay together
Only .tsx files with an export default are compiled as templates. Files without a default export (helper components, utilities) are ignored by the build. This lets you organize templates in subfolders with shared components:
src/templates/
├── invoice/
│ ├── invoice.tsx # Has export default → compiled as template
│ ├── Table.tsx # No default export → ignored (helper component)
│ └── formatters.ts # No default export → ignored (utility)
├── receipt/
│ └── receipt.tsx # Has export default → compiled as template
# Build templates
npx rtpdf build ./src/templates
npx rtpdf build ./src/templates --out ./dist/pdf
# Validate templates (no hooks, no event handlers)
npx rtpdf validate ./src/templates
# Preview a template with mock data
npx rtpdf preview ./src/templates/invoice.tsx --data mock.json
npx rtpdf preview ./src/templates/invoice.tsx --data mock.json --out invoice.pdfimport { generatePDF } from 'react-template-pdf/runtime'
const pdf = await generatePDF('./dist/templates/invoice', data, {
chromiumPath: '/usr/bin/chromium',
})import { PdfEngine } from 'react-template-pdf/runtime'
const engine = new PdfEngine({
chromiumPath: '/usr/bin/chromium',
poolSize: 4, // 4 parallel tabs
})
await engine.start()
const pdf = await engine.generate('./dist/templates/invoice', data)
await engine.stop()await engine.generate(template, data, {
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
landscape: false,
printBackground: true,
headerTemplate: '<div>Header</div>',
footerTemplate: '<div><span class="pageNumber"></span></div>',
displayHeaderFooter: true,
})[DEV] [BUILD] [RUNTIME]
React + TypeScript --> HTML + Handlebars --> transform(payload)
+ Proxy ($) (no React runtime) + Handlebars fill
+ Tailwind + CSS inline + Puppeteer
+ Preview CLI + images base64 --> PDF Buffer
MIT