React PDF Generation with react-pdf (2026)
@react-pdf/renderer lets you build PDFs using React components with a Flexbox layout model — the same mental model as building a web UI. It renders entirely in the browser (or Node.js on the server) with no backend required for simple documents. This guide covers document structure, styling, tables, custom fonts, images, download buttons, and server-side generation with Next.js.
Table of Contents
Setup and Basic Document
npm install @react-pdf/renderer
import { Document, Page, Text, View, StyleSheet, PDFViewer } from '@react-pdf/renderer'
// Every PDF starts with Document → Page → content
function BasicPDF() {
return (
<Document title="My Document" author="Techoral" subject="Guide">
<Page size="A4" style={{ padding: 40, fontFamily: 'Helvetica' }}>
<View style={{ marginBottom: 20 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold', color: '#0f172a' }}>
Hello, PDF World
</Text>
</View>
<Text style={{ fontSize: 12, lineHeight: 1.6, color: '#475569' }}>
This PDF was generated entirely in the browser using React.
</Text>
</Page>
</Document>
)
}
// Preview in browser (development only — large bundle)
function PDFPreviewPage() {
return (
<PDFViewer style={{ width: '100%', height: '80vh' }}>
<BasicPDF />
</PDFViewer>
)
}
// Available Page sizes: A4, LETTER, LEGAL, A3, A5
// Available units: pt (default), mm, cm, in
Styling System
import { StyleSheet } from '@react-pdf/renderer'
// Create styles with StyleSheet.create — same as React Native
const styles = StyleSheet.create({
page: {
flexDirection: 'column',
backgroundColor: '#ffffff',
padding: '40pt',
fontFamily: 'Helvetica',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 30,
paddingBottom: 20,
borderBottomWidth: 2,
borderBottomColor: '#6366f1',
borderBottomStyle: 'solid',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#0f172a',
},
section: {
marginBottom: 20,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
borderBottomStyle: 'solid',
},
label: {
fontSize: 11,
color: '#64748b',
width: '60%',
},
value: {
fontSize: 11,
color: '#0f172a',
fontWeight: 'bold',
textAlign: 'right',
},
// Supported properties: flexbox, color, fontSize, fontWeight,
// margin, padding, border, backgroundColor, width, height,
// textAlign, lineHeight, letterSpacing, textDecoration, opacity
})
Invoice Example
interface InvoiceItem { description: string; qty: number; rate: number }
interface Invoice {
number: string; date: string; dueDate: string
client: { name: string; email: string; address: string }
items: InvoiceItem[]
}
function InvoicePDF({ invoice }: { invoice: Invoice }) {
const subtotal = invoice.items.reduce((s, i) => s + i.qty * i.rate, 0)
const tax = subtotal * 0.1
const total = subtotal + tax
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.title}>INVOICE</Text>
<Text style={{ fontSize: 12, color: '#64748b' }}>#{invoice.number}</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={{ fontSize: 11, color: '#64748b' }}>Date: {invoice.date}</Text>
<Text style={{ fontSize: 11, color: '#ef4444', marginTop: 4 }}>Due: {invoice.dueDate}</Text>
</View>
</View>
{/* Bill To */}
<View style={styles.section}>
<Text style={{ fontSize: 10, color: '#64748b', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Bill To</Text>
<Text style={{ fontSize: 13, fontWeight: 'bold', color: '#0f172a' }}>{invoice.client.name}</Text>
<Text style={{ fontSize: 11, color: '#475569', marginTop: 2 }}>{invoice.client.email}</Text>
<Text style={{ fontSize: 11, color: '#475569', marginTop: 2 }}>{invoice.client.address}</Text>
</View>
{/* Table header */}
<View style={{ flexDirection: 'row', backgroundColor: '#f8fafc', padding: '8pt', borderRadius: 4, marginBottom: 4 }}>
<Text style={{ fontSize: 10, fontWeight: 'bold', color: '#475569', width: '50%' }}>Description</Text>
<Text style={{ fontSize: 10, fontWeight: 'bold', color: '#475569', width: '15%', textAlign: 'center' }}>Qty</Text>
<Text style={{ fontSize: 10, fontWeight: 'bold', color: '#475569', width: '17%', textAlign: 'right' }}>Rate</Text>
<Text style={{ fontSize: 10, fontWeight: 'bold', color: '#475569', width: '18%', textAlign: 'right' }}>Amount</Text>
</View>
{/* Line items */}
{invoice.items.map((item, i) => (
<View key={i} style={styles.row}>
<Text style={{ ...styles.label, width: '50%' }}>{item.description}</Text>
<Text style={{ fontSize: 11, width: '15%', textAlign: 'center', color: '#475569' }}>{item.qty}</Text>
<Text style={{ fontSize: 11, width: '17%', textAlign: 'right', color: '#475569' }}>${item.rate.toFixed(2)}</Text>
<Text style={{ fontSize: 11, width: '18%', textAlign: 'right', color: '#0f172a', fontWeight: 'bold' }}>${(item.qty * item.rate).toFixed(2)}</Text>
</View>
))}
{/* Totals */}
<View style={{ alignItems: 'flex-end', marginTop: 20 }}>
<View style={{ width: '40%' }}>
<View style={styles.row}>
<Text style={{ fontSize: 11, color: '#64748b' }}>Subtotal</Text>
<Text style={{ fontSize: 11, color: '#0f172a' }}>${subtotal.toFixed(2)}</Text>
</View>
<View style={styles.row}>
<Text style={{ fontSize: 11, color: '#64748b' }}>Tax (10%)</Text>
<Text style={{ fontSize: 11, color: '#0f172a' }}>${tax.toFixed(2)}</Text>
</View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, paddingTop: 8, borderTopWidth: 2, borderTopColor: '#6366f1', borderTopStyle: 'solid' }}>
<Text style={{ fontSize: 13, fontWeight: 'bold', color: '#0f172a' }}>Total</Text>
<Text style={{ fontSize: 13, fontWeight: 'bold', color: '#6366f1' }}>${total.toFixed(2)}</Text>
</View>
</View>
</View>
</Page>
</Document>
)
}
Custom Fonts
import { Font } from '@react-pdf/renderer'
// Register before rendering
Font.register({
family: 'Inter',
fonts: [
{ src: '/fonts/Inter-Regular.ttf' },
{ src: '/fonts/Inter-Bold.ttf', fontWeight: 'bold' },
{ src: '/fonts/Inter-Italic.ttf', fontStyle: 'italic' },
],
})
// Use in styles
const styles = StyleSheet.create({
body: { fontFamily: 'Inter', fontSize: 12 },
heading: { fontFamily: 'Inter', fontWeight: 'bold', fontSize: 18 },
})
// Built-in fonts (no registration needed):
// Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
// Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
// Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
Images
import { Image } from '@react-pdf/renderer'
// From URL
<Image src="https://example.com/logo.png" style={{ width: 120, height: 40 }} />
// From local file (Vite/webpack asset import)
import logoSrc from '/public/logo.png'
<Image src={logoSrc} style={{ width: 120, height: 40 }} />
// From base64 data URI
<Image src={`data:image/png;base64,${base64String}`} style={{ width: 200 }} />
// From canvas / chart screenshot
// html2canvas → toDataURL → pass to Image src
Download Button
import { PDFDownloadLink, BlobProvider } from '@react-pdf/renderer'
// Simple download link — renders PDF on click
function DownloadButton({ invoice }: { invoice: Invoice }) {
return (
<PDFDownloadLink
document={<InvoicePDF invoice={invoice} />}
fileName={`invoice-${invoice.number}.pdf`}
>
{({ loading }) => (
<button disabled={loading}>
{loading ? 'Generating PDF...' : 'Download Invoice'}
</button>
)}
</PDFDownloadLink>
)
}
// BlobProvider — more control (open in new tab, upload to S3)
function OpenPDFButton({ invoice }: { invoice: Invoice }) {
return (
<BlobProvider document={<InvoicePDF invoice={invoice} />}>
{({ blob, url, loading }) => (
<button
disabled={loading}
onClick={() => url && window.open(url, '_blank')}
>
{loading ? 'Preparing...' : 'Open PDF'}
</button>
)}
</BlobProvider>
)
}
Server-Side with Next.js
// app/api/invoice/[id]/route.ts
import { renderToBuffer } from '@react-pdf/renderer'
import { InvoicePDF } from '@/components/InvoicePDF'
export async function GET(req: Request, { params }: { params: { id: string } }) {
const invoice = await db.invoice.findUnique({ where: { id: params.id } })
if (!invoice) return new Response('Not found', { status: 404 })
// renderToBuffer runs in Node.js — no browser needed
const buffer = await renderToBuffer(<InvoicePDF invoice={invoice} />)
return new Response(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`,
},
})
}