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.

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"`,
    },
  })
}