PDF generation in React and Next.js, from the components you already have
The usual React paths both compromise: @react-pdf/renderer makes you rebuild the document in its own primitives, and jsPDF/html2canvas rasterize your UI into blurry, unselectable pixels. The third path: render your component to HTML with renderToStaticMarkup, POST it, and get a vector PDF that matches the browser — Tailwind, Grid, web fonts and all.
A Next.js route handler is all it takes
Your invoice component renders to HTML on the server, pdfkitt renders the HTML to PDF, and the route streams it back.
// app/api/invoice/route.ts — Next.js App Router
import { renderToStaticMarkup } from "react-dom/server";
import { Invoice } from "@/components/invoice";
export async function POST(req: Request) {
const data = await req.json();
const html = renderToStaticMarkup(<Invoice {...data} />);
const res = await fetch("https://api.pdfkitt.dev/v1/convert", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFKITT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: { page_size: "A4" } }),
});
return new Response(res.body, {
headers: { "Content-Type": "application/pdf" },
});
}When react-pdf still makes sense
If you need fully offline, client-side generation — a browser extension, a no-backend tool — react-pdf or jsPDF are the right call, since any API needs a network hop. For an app with a backend, server-side rendering keeps one source of truth for markup and produces print-quality output. The Node.js guide covers the same flow outside React, or jump to the API docs.
FAQ
Why not @react-pdf/renderer?
react-pdf is well built, but it is a separate rendering target: you rebuild your document with its own Document/Page/View/Text primitives and its CSS subset - your existing components, Tailwind classes, and print CSS do not carry over. Rendering real HTML server-side means the invoice component you already show on screen is the PDF.
Why not jsPDF or html2canvas in the browser?
Canvas-based capture rasterizes your UI: text becomes pixels, so it blurs on zoom, is not selectable, and file sizes balloon. Layout also depends on the user's browser and screen. Server-side Chromium produces vector text and identical output for every user.
Can I use my Tailwind styles?
Yes. Send a full HTML document that includes your compiled stylesheet - either inline the generated CSS in a style tag or link a public stylesheet URL. Chromium applies it exactly as the browser does, including print utilities.
Does this work in Next.js server components and route handlers?
Yes - the example above is an App Router route handler. renderToStaticMarkup runs in any Node.js server context: route handlers, server actions, API routes, or a background job. Nothing Chromium-related runs in your deployment, so it works on Vercel without special configuration.
What about client-rendered charts - Recharts, Chart.js?
Two options: render the chart to SVG server-side where the library supports it, or set javascript: true in the request so the page's scripts run in sandboxed Chromium before capture (30-second cap). Static SVG is faster; opt-in JavaScript is simpler.
Is my HTML stored anywhere?
No. HTML is processed in memory to render the PDF and is never written to storage or logs. Only request metadata such as timing and status is recorded.
Try pdfkitt free — 1,000 PDFs/month, no credit card
Get an API key and send your first PDF in minutes.