@react-pdf/renderer generates PDF documents from React components — <Document>, <Page>, <View>, <Text>, and <Image> are the core primitives. StyleSheet.create({ style: { ... } }) defines type-checked styles (a subset of Flexbox and CSS). <PDFDownloadLink document={<MyPDF />} fileName="report.pdf"> renders a download link in the browser. <PDFViewer> embeds an inline preview. renderToStream(element) generates a PDF stream on the server for API routes — pipe it directly to the response. Font.register({ family, src }) loads custom fonts. The break prop on View enables manual page breaks; wrap={false} prevents a view from splitting across pages. Tables are built with nested View and Text components since there’s no native table element. Claude Code generates @react-pdf/renderer invoice templates, report PDFs with tables and charts, server-side PDF generation API routes, and browser download link components.
CLAUDE.md for react-pdf
## @react-pdf/renderer Stack
- Version: @react-pdf/renderer >= 3.4
- Primitives: Document > Page > View > Text | Image | Link
- Styles: StyleSheet.create({ flex, flexDirection, fontSize, fontWeight, color, padding, margin })
- Download: <PDFDownloadLink document={<PDF />} fileName="file.pdf">{({ loading }) => ...}</PDFDownloadLink>
- Server: renderToStream(<PDF />) — pipe to Response in Next.js route handler
- Font: Font.register({ family: "Inter", src: "/fonts/Inter.ttf" })
- Page break: <View break /> — manual, or wrap={false} to keep view together
- Image: <Image src="https://..." /> or src={dataUrl} — base64 data URLs work
Invoice PDF Template
// components/pdf/InvoicePDF.tsx — invoice template
import {
Document,
Page,
View,
Text,
Image,
StyleSheet,
Font,
} from "@react-pdf/renderer"
Font.register({
family: "Inter",
fonts: [
{ src: "/fonts/Inter-Regular.ttf" },
{ src: "/fonts/Inter-SemiBold.ttf", fontWeight: 600 },
{ src: "/fonts/Inter-Bold.ttf", fontWeight: 700 },
],
})
const styles = StyleSheet.create({
page: {
fontFamily: "Inter",
fontSize: 10,
color: "#0f172a",
padding: 48,
backgroundColor: "#ffffff",
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 40,
},
logo: {
width: 120,
height: 32,
},
invoiceTitle: {
fontSize: 28,
fontWeight: 700,
color: "#3b82f6",
},
invoiceMeta: {
textAlign: "right",
gap: 4,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 11,
fontWeight: 600,
color: "#64748b",
textTransform: "uppercase",
letterSpacing: 0.5,
marginBottom: 8,
},
addressBlock: {
lineHeight: 1.6,
},
table: {
marginTop: 16,
},
tableHeader: {
flexDirection: "row",
backgroundColor: "#f8fafc",
borderRadius: 4,
padding: "8 12",
fontWeight: 600,
color: "#64748b",
fontSize: 9,
textTransform: "uppercase",
},
tableRow: {
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: "#f1f5f9",
padding: "10 12",
alignItems: "center",
},
colDescription: { flex: 3 },
colQty: { width: 50, textAlign: "center" },
colPrice: { width: 80, textAlign: "right" },
colTotal: { width: 80, textAlign: "right" },
totalsSection: {
marginTop: 16,
alignItems: "flex-end",
},
totalRow: {
flexDirection: "row",
gap: 40,
paddingVertical: 4,
},
totalLabel: {
color: "#64748b",
width: 80,
textAlign: "right",
},
totalValue: {
width: 80,
textAlign: "right",
},
grandTotal: {
fontWeight: 700,
fontSize: 14,
color: "#3b82f6",
},
footer: {
position: "absolute",
bottom: 40,
left: 48,
right: 48,
borderTopWidth: 1,
borderTopColor: "#e2e8f0",
paddingTop: 12,
flexDirection: "row",
justifyContent: "space-between",
color: "#94a3b8",
fontSize: 9,
},
})
interface InvoiceItem {
description: string
quantity: number
unitPriceCents: number
}
interface InvoiceData {
invoiceNumber: string
issueDate: string
dueDate: string
from: { name: string; address: string; email: string }
to: { name: string; address: string; email: string }
items: InvoiceItem[]
taxRate?: number
notes?: string
}
function LineItem({ item }: { item: InvoiceItem }) {
const total = item.quantity * item.unitPriceCents
return (
<View style={styles.tableRow} wrap={false}>
<Text style={styles.colDescription}>{item.description}</Text>
<Text style={styles.colQty}>{item.quantity}</Text>
<Text style={styles.colPrice}>${(item.unitPriceCents / 100).toFixed(2)}</Text>
<Text style={styles.colTotal}>${(total / 100).toFixed(2)}</Text>
</View>
)
}
export function InvoicePDF({ invoice }: { invoice: InvoiceData }) {
const subtotalCents = invoice.items.reduce(
(sum, i) => sum + i.quantity * i.unitPriceCents, 0
)
const taxCents = Math.round(subtotalCents * (invoice.taxRate ?? 0))
const totalCents = subtotalCents + taxCents
return (
<Document title={`Invoice ${invoice.invoiceNumber}`} author="Your Company">
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.invoiceTitle}>INVOICE</Text>
<Text style={{ color: "#64748b", marginTop: 4 }}>#{invoice.invoiceNumber}</Text>
</View>
<View style={styles.invoiceMeta}>
<Text style={{ fontWeight: 600 }}>Your Company Inc.</Text>
<Text style={{ color: "#64748b" }}>Issue: {invoice.issueDate}</Text>
<Text style={{ color: "#64748b" }}>Due: {invoice.dueDate}</Text>
</View>
</View>
{/* From / To */}
<View style={{ flexDirection: "row", gap: 40, marginBottom: 32 }}>
<View style={{ flex: 1 }}>
<Text style={styles.sectionTitle}>From</Text>
<View style={styles.addressBlock}>
<Text style={{ fontWeight: 600 }}>{invoice.from.name}</Text>
<Text style={{ color: "#64748b" }}>{invoice.from.address}</Text>
<Text style={{ color: "#64748b" }}>{invoice.from.email}</Text>
</View>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.sectionTitle}>Bill To</Text>
<View style={styles.addressBlock}>
<Text style={{ fontWeight: 600 }}>{invoice.to.name}</Text>
<Text style={{ color: "#64748b" }}>{invoice.to.address}</Text>
<Text style={{ color: "#64748b" }}>{invoice.to.email}</Text>
</View>
</View>
</View>
{/* Items table */}
<View style={styles.table}>
<View style={styles.tableHeader}>
<Text style={styles.colDescription}>Description</Text>
<Text style={styles.colQty}>Qty</Text>
<Text style={styles.colPrice}>Unit Price</Text>
<Text style={styles.colTotal}>Total</Text>
</View>
{invoice.items.map((item, i) => <LineItem key={i} item={item} />)}
</View>
{/* Totals */}
<View style={styles.totalsSection}>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Subtotal</Text>
<Text style={styles.totalValue}>${(subtotalCents / 100).toFixed(2)}</Text>
</View>
{invoice.taxRate && (
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Tax ({(invoice.taxRate * 100).toFixed(0)}%)</Text>
<Text style={styles.totalValue}>${(taxCents / 100).toFixed(2)}</Text>
</View>
)}
<View style={{ ...styles.totalRow, marginTop: 8 }}>
<Text style={{ ...styles.totalLabel, ...styles.grandTotal }}>Total</Text>
<Text style={{ ...styles.totalValue, ...styles.grandTotal }}>
${(totalCents / 100).toFixed(2)}
</Text>
</View>
</View>
{invoice.notes && (
<View style={{ marginTop: 32 }}>
<Text style={styles.sectionTitle}>Notes</Text>
<Text style={{ color: "#64748b", lineHeight: 1.6 }}>{invoice.notes}</Text>
</View>
)}
{/* Footer */}
<View style={styles.footer} fixed>
<Text>Thank you for your business</Text>
<Text>Invoice #{invoice.invoiceNumber}</Text>
</View>
</Page>
</Document>
)
}
Download Link Component
// components/InvoiceDownloadButton.tsx
"use client"
import { PDFDownloadLink } from "@react-pdf/renderer"
import { InvoicePDF } from "./pdf/InvoicePDF"
import type { InvoiceData } from "@/types/invoice"
export function InvoiceDownloadButton({ invoice }: { invoice: InvoiceData }) {
return (
<PDFDownloadLink
document={<InvoicePDF invoice={invoice} />}
fileName={`invoice-${invoice.invoiceNumber}.pdf`}
>
{({ blob, url, loading, error }) => (
<button
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-md text-sm disabled:opacity-50"
>
{loading ? "Generating PDF..." : "Download Invoice PDF"}
</button>
)}
</PDFDownloadLink>
)
}
Server-Side PDF Generation
// app/api/invoices/[id]/pdf/route.ts — stream PDF from API route
import { renderToStream } from "@react-pdf/renderer"
import { InvoicePDF } from "@/components/pdf/InvoicePDF"
import { getInvoice } from "@/lib/invoice-service"
import { auth } from "@/lib/auth"
export async function GET(
_req: Request,
{ params }: { params: { id: string } }
) {
const session = await auth()
if (!session?.user) {
return new Response("Unauthorized", { status: 401 })
}
const invoice = await getInvoice(params.id, session.user.id)
if (!invoice) return new Response("Not found", { status: 404 })
const stream = await renderToStream(
<InvoicePDF invoice={invoice} />
)
// Convert to ReadableStream for Response
const readable = new ReadableStream({
start(controller) {
stream.on("data", chunk => controller.enqueue(chunk))
stream.on("end", () => controller.close())
stream.on("error", err => controller.error(err))
},
})
return new Response(readable, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${invoice.invoiceNumber}.pdf"`,
"Cache-Control": "private, no-cache",
},
})
}
For the Puppeteer/html-pdf alternative when complex HTML/CSS layouts that are difficult to express in react-pdf’s Flexbox subset are needed — Puppeteer launches a headless Chrome instance to render HTML to PDF with full CSS support including grid layouts and web fonts, but adds significant memory overhead and cold start time in serverless environments, see the PDF generation comparison guide. For the pdfkit alternative when lower-level programmatic PDF construction with precise positioning control is needed without a React component model — pdfkit provides a Canvas-like drawing API with .text(), .rect(), .image() at specific x/y coordinates, useful for complex multi-page reports with precise typography, see the programmatic document generation guide. The Claude Skills 360 bundle includes react-pdf skill sets covering invoice templates, server-side streaming, and custom fonts. Start with the free tier to try PDF generation code generation.