Skip to main content
Back to Blog
Tutorials
3 min read
November 9, 2024

How to Generate PDFs in Next.js with React-PDF

Generate invoices, reports, and documents as PDFs in Next.js using @react-pdf/renderer. Dynamic content, tables, and styling.

Ryel Banfield

Founder & Lead Developer

Generate professional invoices, reports, and documents as downloadable PDFs using React components.

Step 1: Install React-PDF

pnpm add @react-pdf/renderer

Step 2: Create an Invoice Template

// components/pdf/InvoiceDocument.tsx
import {
  Document,
  Page,
  Text,
  View,
  StyleSheet,
  Font,
} from "@react-pdf/renderer";

Font.register({
  family: "Inter",
  src: "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZhrib2Bg-4.ttf",
});

const styles = StyleSheet.create({
  page: {
    padding: 40,
    fontFamily: "Inter",
    fontSize: 10,
    color: "#1a1a1a",
  },
  header: {
    flexDirection: "row",
    justifyContent: "space-between",
    marginBottom: 30,
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
    color: "#2563eb",
  },
  companyInfo: {
    textAlign: "right",
    fontSize: 9,
    color: "#6b7280",
  },
  section: {
    marginBottom: 20,
  },
  sectionTitle: {
    fontSize: 11,
    fontWeight: "bold",
    marginBottom: 8,
    color: "#374151",
  },
  table: {
    borderWidth: 1,
    borderColor: "#e5e7eb",
    borderRadius: 4,
  },
  tableHeader: {
    flexDirection: "row",
    backgroundColor: "#f3f4f6",
    padding: 8,
    borderBottomWidth: 1,
    borderBottomColor: "#e5e7eb",
  },
  tableRow: {
    flexDirection: "row",
    padding: 8,
    borderBottomWidth: 1,
    borderBottomColor: "#e5e7eb",
  },
  colDescription: { flex: 3 },
  colQty: { flex: 1, textAlign: "center" },
  colRate: { flex: 1, textAlign: "right" },
  colAmount: { flex: 1, textAlign: "right" },
  bold: { fontWeight: "bold" },
  totalsRow: {
    flexDirection: "row",
    justifyContent: "flex-end",
    marginTop: 4,
    paddingHorizontal: 8,
    paddingVertical: 4,
  },
  footer: {
    position: "absolute",
    bottom: 30,
    left: 40,
    right: 40,
    textAlign: "center",
    fontSize: 8,
    color: "#9ca3af",
  },
});

interface InvoiceItem {
  description: string;
  quantity: number;
  rate: number;
}

interface InvoiceData {
  invoiceNumber: string;
  date: string;
  dueDate: string;
  client: {
    name: string;
    address: string;
    email: string;
  };
  items: InvoiceItem[];
  notes?: string;
}

export function InvoiceDocument({ data }: { data: InvoiceData }) {
  const subtotal = data.items.reduce(
    (sum, item) => sum + item.quantity * item.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>#{data.invoiceNumber}</Text>
          </View>
          <View style={styles.companyInfo}>
            <Text style={styles.bold}>RCB Software</Text>
            <Text>123 Business Street</Text>
            <Text>hello@rcbsoftware.com</Text>
          </View>
        </View>

        {/* Client & Dates */}
        <View style={{ flexDirection: "row", marginBottom: 20 }}>
          <View style={{ flex: 1 }}>
            <Text style={styles.sectionTitle}>Bill To</Text>
            <Text style={styles.bold}>{data.client.name}</Text>
            <Text>{data.client.address}</Text>
            <Text>{data.client.email}</Text>
          </View>
          <View style={{ flex: 1, textAlign: "right" }}>
            <Text>Date: {data.date}</Text>
            <Text>Due Date: {data.dueDate}</Text>
          </View>
        </View>

        {/* Line Items */}
        <View style={styles.table}>
          <View style={styles.tableHeader}>
            <Text style={[styles.colDescription, styles.bold]}>
              Description
            </Text>
            <Text style={[styles.colQty, styles.bold]}>Qty</Text>
            <Text style={[styles.colRate, styles.bold]}>Rate</Text>
            <Text style={[styles.colAmount, styles.bold]}>Amount</Text>
          </View>
          {data.items.map((item, i) => (
            <View key={i} style={styles.tableRow}>
              <Text style={styles.colDescription}>{item.description}</Text>
              <Text style={styles.colQty}>{item.quantity}</Text>
              <Text style={styles.colRate}>${item.rate.toFixed(2)}</Text>
              <Text style={styles.colAmount}>
                ${(item.quantity * item.rate).toFixed(2)}
              </Text>
            </View>
          ))}
        </View>

        {/* Totals */}
        <View style={{ marginTop: 10 }}>
          <View style={styles.totalsRow}>
            <Text style={{ width: 100 }}>Subtotal:</Text>
            <Text style={{ width: 80, textAlign: "right" }}>
              ${subtotal.toFixed(2)}
            </Text>
          </View>
          <View style={styles.totalsRow}>
            <Text style={{ width: 100 }}>Tax (10%):</Text>
            <Text style={{ width: 80, textAlign: "right" }}>
              ${tax.toFixed(2)}
            </Text>
          </View>
          <View
            style={[
              styles.totalsRow,
              {
                borderTopWidth: 1,
                borderTopColor: "#e5e7eb",
                paddingTop: 8,
              },
            ]}
          >
            <Text style={[{ width: 100 }, styles.bold]}>Total:</Text>
            <Text style={[{ width: 80, textAlign: "right" }, styles.bold]}>
              ${total.toFixed(2)}
            </Text>
          </View>
        </View>

        {/* Notes */}
        {data.notes && (
          <View style={[styles.section, { marginTop: 30 }]}>
            <Text style={styles.sectionTitle}>Notes</Text>
            <Text>{data.notes}</Text>
          </View>
        )}

        {/* Footer */}
        <Text style={styles.footer}>
          Thank you for your business. Payment due within 30 days.
        </Text>
      </Page>
    </Document>
  );
}

Step 3: Client-Side Download Button

"use client";

import { pdf } from "@react-pdf/renderer";
import { InvoiceDocument } from "@/components/pdf/InvoiceDocument";

export function DownloadInvoiceButton({ data }: { data: InvoiceData }) {
  async function handleDownload() {
    const blob = await pdf(<InvoiceDocument data={data} />).toBlob();
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `invoice-${data.invoiceNumber}.pdf`;
    link.click();
    URL.revokeObjectURL(url);
  }

  return (
    <button
      onClick={handleDownload}
      className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
    >
      Download Invoice PDF
    </button>
  );
}

Step 4: Server-Side PDF Generation (API Route)

// app/api/invoice/[id]/pdf/route.tsx
import { NextRequest, NextResponse } from "next/server";
import { renderToBuffer } from "@react-pdf/renderer";
import { InvoiceDocument } from "@/components/pdf/InvoiceDocument";

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  // Fetch invoice data from database
  const invoice = await getInvoiceById(id);
  if (!invoice) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  const buffer = await renderToBuffer(
    <InvoiceDocument data={invoice} />
  );

  return new NextResponse(buffer, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
    },
  });
}

Step 5: PDF Preview in Browser

"use client";

import { useState } from "react";
import { PDFViewer } from "@react-pdf/renderer";
import { InvoiceDocument } from "@/components/pdf/InvoiceDocument";

export function InvoicePreview({ data }: { data: InvoiceData }) {
  return (
    <PDFViewer width="100%" height={600} className="rounded-lg border">
      <InvoiceDocument data={data} />
    </PDFViewer>
  );
}

Step 6: Multi-Page Report

<Document>
  {/* Cover Page */}
  <Page size="A4" style={styles.page}>
    <Text style={styles.title}>Monthly Report</Text>
    <Text>{data.period}</Text>
  </Page>

  {/* Summary Page */}
  <Page size="A4" style={styles.page}>
    <Text style={styles.sectionTitle}>Executive Summary</Text>
    <Text>{data.summary}</Text>
  </Page>

  {/* Detail Pages - one per section */}
  {data.sections.map((section, i) => (
    <Page key={i} size="A4" style={styles.page}>
      <Text style={styles.sectionTitle}>{section.title}</Text>
      {/* section content */}
    </Page>
  ))}
</Document>

Need Document Generation?

We build web applications with PDF generation, invoice systems, and automated document workflows. Contact us to discuss your project.

PDFReact-PDFNext.jsdocument generationtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles