Get started
react-pdf vs Playwright: which one for PDF generation in 2026?

react-pdf vs Playwright: which one for PDF generation in 2026?

react-pdf vs Playwright for PDF: when to use each, what react-pdf can't do, why most React teams switch to HTML to PDF for production.

14 min read

Most React teams pick @react-pdf/renderer because the name is right next to React in npm, the API feels idiomatic, and the first one-page receipt renders cleanly. Then the requirements grow: a multi-page invoice with a 200-row line item table, a chart, a custom font with three weights, the design system used on the rest of the app. That is the point where the elegant DSL stops scaling, and most teams either rewrite the whole pipeline or bolt on a Playwright-based escape hatch. This article walks through what each tool actually does, where react-pdf hits its wall, and how to migrate without throwing away the React component you already have.

TL;DR

react-pdf is fine for simple one-page PDFs you can ship from the browser. For anything multi-page, table-heavy, font-customized, or driven by an existing design system, server-rendered HTML to PDF via Playwright wins, not because Playwright is more capable as a renderer (it is also Chromium) but because you stop fighting a custom layout DSL and reuse the CSS, fonts and components you already have on your site.

What react-pdf is, and what it isn't

@react-pdf/renderer is a custom React reconciler that compiles a tree of bespoke primitives (<Document>, <Page>, <View>, <Text>, <Image>) into a PDF byte stream. It is not a browser, not a CSS engine, and not a wrapper around Chromium. It is a JSX-shaped API on top of a small layout engine (Yoga, the same Facebook engine used by React Native) that knows how to draw rectangles, text runs and images at fixed page coordinates. (react-pdf docs)

That distinction explains every limitation later in this article. There is no DOM, so anything that depends on a browser API (Canvas, IntersectionObserver, getComputedStyle) is absent. There is no CSS parser, so class names, media queries and @font-face declarations are silently ignored. There is no print backend that knows how to break a row across two pages, so layout overflow is handled by snapping whole <View> blocks to the next page.

A minimal invoice in react-pdf looks like this:

import { Document, Page, Text, View, StyleSheet } from "@react-pdf/renderer";
 
const styles = StyleSheet.create({
  page: { padding: 40, fontSize: 12, fontFamily: "Helvetica" },
  header: { fontSize: 20, marginBottom: 20 },
  row: { flexDirection: "row", borderBottom: 1, padding: 4 },
  cell: { flex: 1 },
});
 
export function Invoice({ items }: { items: Array<{ name: string; price: number }> }) {
  return (
    <Document>
      <Page size="A4" style={styles.page}>
        <Text style={styles.header}>Invoice INV-001</Text>
        {items.map((item, i) => (
          <View key={i} style={styles.row}>
            <Text style={styles.cell}>{item.name}</Text>
            <Text style={styles.cell}>${item.price}</Text>
          </View>
        ))}
      </Page>
    </Document>
  );
}

That code is genuinely elegant for a one-pager. It runs in the browser, it has no Chromium dependency, the bundle adds around 600 KB gzipped, and it produces a deterministic PDF in 100 to 300 ms. The npm package sits at 860k weekly downloads as of mid-2026 (npmtrends), so a large slice of the React ecosystem hits this exact code shape.

The trouble is that the abstraction does not stretch. As soon as the document needs anything beyond stacked flex rows, the gap between "what a designer drew" and "what <View> can express" gets wide.

What Playwright + HTML brings instead

Playwright is Microsoft's browser automation library. Its page.pdf() method drives a real headless Chromium instance through the Chrome DevTools Protocol, producing a PDF from whatever HTML the page contains. (Playwright docs) For React teams, the model is: render the component to HTML with renderToStaticMarkup, push that HTML into the page, then print.

import { chromium } from "playwright";
import { renderToStaticMarkup } from "react-dom/server";
import { Invoice } from "./Invoice";
 
const html = `
  <!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="https://cdn.example.com/app.css" />
      <style>@page { size: A4; margin: 20mm; }</style>
    </head>
    <body>${renderToStaticMarkup(<Invoice items={items} />)}</body>
  </html>
`;
 
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle" });
const pdf = await page.pdf({ format: "A4", printBackground: true });
await browser.close();

The Invoice component here is a normal React component. It uses normal CSS (or Tailwind, or MUI, or styled-components). It is the same component you would render on a webpage, with maybe a @media print tweak. There is no DSL to learn, no Font.register() to call, no flex-only subset of CSS to remember.

In production both paths look very different from the "5 lines of code" tutorials. The react-pdf path stays small but rewrites every UI in the custom DSL. The Playwright path keeps the UI but adds Chromium ops (Docker image size, browser pooling, crash recovery). The third option, a managed HTML to PDF API, removes the Chromium ops and turns the call into a single fetch.

Head-to-head comparison

Factor@react-pdf/rendererPlaywright + HTML
Layout engineYoga flexbox over PDF primitivesFull Chromium (Blink)
CSS supportInline style={} only, flex subsetFull CSS 3: Grid, Flexbox, transforms, gradients
<table> with auto-paginationNo native supportNative via <thead> repeated by Chromium
Page breaksManual via <Page break> or wrap={false}CSS break-before: page, break-inside: avoid
@media printNot supported (no concept of media)Full support
Custom fontsFont.register() per weight, TTF/WOFF only@font-face, any browser-supported format
Charts (Recharts, Chart.js)Not supported (no DOM)Works as in browser
SVGPartial (custom <Svg> primitive subset)Full browser SVG
Tailwind / MUI / design systemNo (class names ignored)Full reuse of existing CSS
RTL and CJK scriptsLimited, needs font registrationNative browser support
Cold-start latency0 ms (no browser)400 to 600 ms
Warm latency100 to 300 ms50 to 200 ms
Bundle / dependency size~600 KB gzipped (client)~300 MB Chromium (server)
Runs in browserYesNo
Runs in serverlessYes (if bundled client-side)Hard (Chromium size)
Print preview fidelityProprietary rendererIdentical to Chrome's print dialog
DebuggingRun the renderer, inspect output PDFDevTools on page.setContent()
Accessibility / tagged PDFNo PDF/UA supportPossible via PDF/UA pipeline

The factor that changes the calculus for most teams is row five: existing CSS reuse. If the PDF needs to look like the rest of the app, the second column wins for free; the first column requires a full re-implementation of the design in flex primitives.

Five scenarios where react-pdf hits a wall

1. Multi-page invoice with overflowing line items

Symptom: an invoice with 80 line items fits on two pages on a designer's screen. In react-pdf, rows are nested <View> elements, the table header does not repeat on page 2, and the totals block sometimes lands on its own orphan page.

Workaround: split the items into chunks of N rows per page (you have to compute N by hand from font size and row height), render one <Page> per chunk, duplicate the header <View> inside each <Page>, and pin the totals to a fixed <Page> at the end. This works but it is layout code that should not exist; you are pre-paginating in JavaScript what the print engine should paginate for you.

Playwright equivalent: a normal HTML <table> with <thead> and <tbody>. Chromium repeats <thead> on every printed page automatically (CSS 2.1 paged media) and breaks rows at row boundaries.

2. Charts embedded in the PDF

Symptom: the design includes a sales chart. Recharts, Chart.js, Apex, Nivo: none of them render in @react-pdf/renderer because they need a DOM and a Canvas or SVG that the renderer can read.

Workaround: pre-render the chart server-side to a static SVG (with victory in headless mode, or d3 rendered through jsdom), then embed the SVG via the react-pdf <Image> primitive or its custom <Svg> primitive subset. You lose the interactive chart library's defaults; you reimplement axes and legends by hand.

Playwright equivalent: drop the chart component on the page as you would in the browser. Recharts renders to SVG, Chromium prints the SVG, done.

3. Custom fonts with multiple weights

Symptom: the brand requires Inter at 400, 500 and 700 weights plus italic. In react-pdf you call Font.register four times, one per file, passing explicit weight metadata. The font file must be TTF or WOFF (no WOFF2). If a glyph is missing the renderer throws at print time.

Workaround: host the four font files at a URL the Node server can fetch, register each one with explicit fontWeight and fontStyle, and bundle a fallback for missing glyphs.

Playwright equivalent: a single @font-face block, or a Google Fonts <link> tag, with font-display: swap and a waitUntil: 'networkidle' to make sure the font is loaded before printing.

4. Right-to-left or non-Latin scripts

Symptom: Arabic, Hebrew, Chinese, Japanese, Korean. The default react-pdf fonts (Helvetica, Times-Roman, Courier from the PDF base 14 set) do not contain these glyphs. Bidirectional text shaping is also limited.

Workaround: register a CJK or Arabic font (Noto Sans CJK, Noto Sans Arabic) explicitly, then audit every <Text> for correct bidirectional ordering. RTL layout flips are not automatic.

Playwright equivalent: Chromium has full HarfBuzz text shaping and bidirectional handling. Any font with the right glyphs prints correctly without configuration.

5. Reusing an existing design system

Symptom: the team has a design system (Tailwind, MUI, Chakra, a custom one). The PDF must look like the rest of the app. In react-pdf, none of those CSS layers exist. Every component must be rewritten in <View> and <Text> with inline styles.

Workaround: there is no workaround at the framework level. You build a parallel design system inside react-pdf. Teams typically end up with two component libraries: one for the web, one for PDF, that drift over time.

Playwright equivalent: the same component, the same CSS, the same design system. You ship the build of your existing CSS, link it in the HTML wrapper, and Chromium does the rest.

When react-pdf is still the right pick

The above is not an argument against @react-pdf/renderer. It is an argument against using it for documents it was not designed for. There are real cases where it remains the better tool:

  • Single-page badges, tickets, certificates. Fixed-size content with no overflow risk. The DSL constraints are not constraints when the document is one page.
  • Pure-client browser bundle. If the PDF must be generated in the browser with no backend, no Chromium, no API call, react-pdf is one of the few options that fits. The bundle adds around 600 KB gzipped, which is acceptable for a single feature.
  • Offline-first apps. Same constraint as above: no network, no Chromium, but you do need a PDF.
  • Tiny bundle size matters more than fidelity. Embedded admin tools, internal dashboards, anything where shipping 300 MB of Chromium or paying per-render to an API is not justified.
  • Strict CPU and memory budget. A Chromium-based pipeline holds 200 to 400 MB of memory per browser instance. react-pdf runs in the parent Node process and uses tens of MB.

If your use case sits in this list, stay with react-pdf. The rest of this article assumes you have outgrown it.

Migrating from react-pdf to Playwright + HTML

The migration is less painful than it sounds because the React component tree can stay the same; only the primitives change. A practical pattern:

Step 1: extract a static HTML version of your react-pdf document. Take the JSX, strip the <Document> and <Page> wrappers, replace <View> with <div>, <Text> with <span> or <p>, and convert the StyleSheet.create({...}) block into CSS classes (or inline style={} if you prefer). At this point you have a normal React component that renders HTML.

Step 2: restyle with normal CSS. You can keep the flex inline styles, but most teams take this as the opportunity to move to a stylesheet (Tailwind, CSS modules, styled-components, plain CSS). Whatever your app already uses works here.

Step 3: add @media print page breaks. This is the part where Chromium does real work for you. Add break-before: page on the totals block, break-inside: avoid on rows that should not split, thead { display: table-header-group } so the header repeats. (MDN: @media print, MDN: break-before)

Step 4: call page.pdf() or a managed API. Render the component to HTML on the server (renderToStaticMarkup), inject the result into a minimal HTML wrapper, and feed it to Playwright or to an HTTP endpoint.

Before, the react-pdf invoice row:

<View style={{ flexDirection: "row", borderBottom: 1 }}>
  <Text style={{ flex: 1 }}>{item.name}</Text>
  <Text style={{ flex: 1, textAlign: "right" }}>${item.price}</Text>
</View>

After, the same row as HTML with CSS:

<tr className="row">
  <td>{item.name}</td>
  <td className="amount">${item.price}</td>
</tr>

The work is the same in scale of code. The win is that the second version uses a real <table> that paginates by itself, can be styled with the same CSS as the rest of the site, and renders charts and custom fonts without escape hatches.

A third option: PDF4.dev or a managed HTML to PDF API

If the reason you picked react-pdf was to avoid running Chromium yourself, switching to Playwright reintroduces exactly that operational cost. Docker images grow by 700 MB to 1 GB, you need a browser pool, you need crash recovery, you need to handle the 250 MB AWS Lambda size limit. Some teams accept that; many do not.

PDF4.dev runs the same Chromium engine as Playwright, on our infrastructure, behind a single HTTP endpoint. You render your React component to HTML in your app exactly as you would for Playwright, then POST the HTML to /api/v1/render. The trade-off is per-render cost (a few cents per thousand PDFs on a paid plan, free tier for prototyping) versus zero Chromium operations.

curl -X POST https://pdf4.dev/api/v1/render \
  -H "Authorization: Bearer p4_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!doctype html><html>...</html>",
    "format": { "preset": "a4", "margins": { "top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm" } }
  }'

The HTML you POST is the same HTML you would feed to Playwright. The output PDF is byte-identical (same Chromium version). The only thing PDF4.dev removes is the Chromium operational layer. Try the free HTML to PDF tool in your browser without installing anything.

The broader DIY-vs-managed framing is covered in the Puppeteer comparison; the same trade-offs apply to Playwright.

Frequently asked questions

Is react-pdf good for production? Yes, for the narrow set of documents it was designed for: single-page or short multi-page PDFs with predictable content. For multi-page invoices, charts, or reuse of an existing design system, the layout model runs out of room.

Can react-pdf render tables that span multiple pages? Not natively. Tables are built from nested <View> rows; they do not paginate as tables and headers do not repeat across pages.

How do I add custom fonts to react-pdf? Call Font.register({ family, src }) for each weight and style, then reference the family in a StyleSheet. TTF or WOFF only.

Is Playwright faster than react-pdf? Cold-start, react-pdf is faster (no browser). Warm and pooled, Playwright is faster per render. Pick based on your traffic pattern.

Can I use Tailwind CSS with react-pdf? No. Class names are ignored. Tailwind requires a real browser, which means a Chromium-based path.

Does Playwright work in Next.js? Yes in long-running Node servers. Not in edge runtimes. On Vercel serverless functions you need workarounds like @sparticuz/chromium or a managed PDF API.

How do I generate a PDF from a React component in Next.js? Either render the component to HTML and print with Playwright, or render to HTML and POST to a managed API. See the Next.js PDF generation guide for the full pattern.

Can react-pdf render charts from Recharts or Chart.js? Not directly. Pre-render the chart as SVG and embed it as an image, or use a Chromium-based path.

For the broader picture, see convert a React component to PDF, Playwright vs Puppeteer for PDF, and the Node.js PDF generation guide. For the pure-PDF-library angle (no React, no browser), see pdf-lib vs jsPDF vs PDFKit.

If you want to test the HTML path without installing anything, the free HTML to PDF tool on PDF4.dev runs the same Chromium pipeline in your browser.

Free tools mentioned:

Html To PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.