# HTML to PDF in React Generate PDFs from React with react-to-print, @react-pdf/renderer, jsPDF with html2canvas, or server-side rendering to a PDF API, with tradeoffs. "Export this page to PDF" sounds simple in React, but the options behave very differently. Some use the browser's print dialog, some draw a screenshot into a PDF, and one doesn't use HTML at all. The most reliable approach renders on the server. Here is what each one does, and when to use it. **Quick answer.** Render your component to HTML on the server with `renderToStaticMarkup`, then send it to the API from your backend (never from the browser). ```bash curl -X POST https://html-pdf.com/v1/pdf \ -H "Authorization: Bearer $HTML_PDF_KEY" \ -H "Content-Type: application/json" \ -d '{"html": "

Invoice #042

"}' \ -o invoice.pdf ``` ## React PDF options compared This table shows what each approach produces and where it runs. | Approach | Output | Selectable text | Uses your HTML/CSS | Runs | |---|---|---|---|---| | `react-to-print` / `window.print()` | Whatever the user saves from the print dialog | Yes | Yes | Browser, user clicks through a dialog | | `@react-pdf/renderer` | Real PDF | Yes | No (its own components and styles) | Browser or Node | | jsPDF + html2canvas | Image of the DOM inside a PDF | No (with the image approach) | Roughly | Browser | | Server render + html-pdf.com | Chromium PDF | Yes | Yes | Your backend | ## Option 1: react-to-print (the print dialog) `react-to-print` lets the user print one component instead of the whole page. In v3 you pass a ref. ```jsx import { useRef } from "react"; import { useReactToPrint } from "react-to-print"; export function InvoicePage({ invoice }) { const contentRef = useRef(null); const print = useReactToPrint({ contentRef, documentTitle: `invoice-${invoice.id}` }); return ( <>
); } ``` Style the printed version with `@media print` and `@page { size: A4; margin: 15mm }`. It costs nothing and keeps your exact CSS. The downside is that the user has to pick "Save as PDF" in a dialog, and margins, headers and scaling depend on their browser settings. You also can't email or store the file, because it never reaches your code. ## Option 2: @react-pdf/renderer (not HTML) `@react-pdf/renderer` builds real PDFs from React components. Those components are its own primitives, not DOM elements, so your existing JSX and CSS classes don't carry over. ```jsx import { Document, Page, Text, View, StyleSheet, PDFDownloadLink } from "@react-pdf/renderer"; const styles = StyleSheet.create({ page: { padding: 40 }, row: { flexDirection: "row", justifyContent: "space-between" }, }); const InvoicePdf = ({ invoice }) => ( Invoice #{invoice.number} Design work $120.00 ); export const DownloadButton = ({ invoice }) => ( } fileName="invoice.pdf"> Download PDF ); ``` It is a good choice when you design the PDF from scratch and want precise control. The cost is a second version of every layout, with a subset of CSS and fonts you register yourself. ## Option 3: jsPDF + html2canvas This approach gives you a PDF with no server. It captures the DOM as a canvas and places the image in a PDF. ```js import html2canvas from "html2canvas"; import { jsPDF } from "jspdf"; async function exportPdf(element) { const canvas = await html2canvas(element, { scale: 2 }); const pdf = new jsPDF({ unit: "mm", format: "a4" }); const width = pdf.internal.pageSize.getWidth(); const height = (canvas.height * width) / canvas.width; pdf.addImage(canvas.toDataURL("image/png"), "PNG", 0, 0, width, height); pdf.save("invoice.pdf"); } ``` The result is a picture. Text isn't selectable or searchable, files are large, and content longer than one page needs manual slicing. html2canvas also re-implements CSS rendering, so some styles come out differently. ## Option 4: render on the server, convert with Chromium For PDFs that look exactly like your components, have selectable text and can be emailed or stored, render the component to HTML on your backend and send it to a headless Chromium. html-pdf.com runs the Chromium part. **Never call the API from the browser.** Anyone could read your API key from the network tab and spend your credits. The browser calls your backend, and your backend calls html-pdf.com. Get a key and keep it in a server-side environment variable. Open the link we email you to confirm your address, which adds 100 free credits. ```bash curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}' ``` ### Backend route (Node + Express) ```jsx // server.jsx: run with a JSX-aware runtime such as `tsx server.jsx` import express from "express"; import { renderToStaticMarkup } from "react-dom/server"; import { Invoice } from "./src/Invoice.jsx"; const app = express(); const css = `body { font-family: system-ui, sans-serif } .row { display: flex; justify-content: space-between }`; app.get("/api/invoices/:id/pdf", async (req, res) => { const invoice = await loadInvoice(req.params.id); // check the user may see it const html = "" + renderToStaticMarkup(); const r = await fetch("https://html-pdf.com/v1/pdf", { method: "POST", headers: { Authorization: `Bearer ${process.env.HTML_PDF_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ html, css, format: "A4", margin: "15mm", max_pages: 20 }), }); if (!r.ok) { const { error } = await r.json(); return res.status(502).json({ message: error.message }); } res.set("Content-Type", "application/pdf"); res.set("Content-Disposition", `attachment; filename="invoice-${invoice.id}.pdf"`); res.send(Buffer.from(await r.arrayBuffer())); }); app.listen(3001); ``` `renderToStaticMarkup` outputs plain HTML without React's hydration attributes. Components should render their data synchronously, because effects and client-side fetching don't run on the server. If you use Tailwind or CSS modules, pass the built stylesheet in `css`, or inline it in a `