Claude Code for React PDF: Generate PDFs with React Components — Claude Skills 360 Blog
Blog / Frontend / Claude Code for React PDF: Generate PDFs with React Components
Frontend

Claude Code for React PDF: Generate PDFs with React Components

Published: March 28, 2027
Read time: 7 min read
By: Claude Skills 360

@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>
  )
}
// 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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free