PDFreact-print-pdf
DocsBrowser-only alpha

Best practices

Patterns that ship, anti-patterns that bite, and the small rules that keep PDF output identical to what you see on screen.

Edit this page

This page is the cheat sheet for shipping react-print-pdf in a real product. The full API surface is in API reference; this is the "what should I actually do" version.

Use <Printable> unless you have a reason not to

The <Printable> wrapper is the recommended entry point. It:

  • mounts your component at the correct content-area width (paper width − margins)
  • exposes a ref for the export hook
  • adds a data-rpp-printable marker so the walker can find it reliably
  • works on visible content, hidden content, or off-screen content
import { usePDFExport, Printable, ExportButton } from "react-print-pdf/react";

function InvoicePage() {
  const exporter = usePDFExport({
    fileName: "invoice-2024-001.pdf",
    paperSize: "A4",
    margins: { top: 40, right: 48, bottom: 40, left: 48 },
  });

  return (
    <>
      <ExportButton target={exporter.ref}>Download PDF</ExportButton>
      <Printable ref={exporter.ref} options={exporter.options}>
        <Invoice data={data} />
      </Printable>
    </>
  );
}

Drop down to raw exportToPDF(element, options) only if you need to export something that lives outside your component tree (e.g. a third-party widget you don't control) or you want to script multi-document batch exports.

Match the on-screen width to the content area

The single largest cause of "the PDF doesn't look like the screen" tickets is rendering at one width and exporting at another. The component reflows, line breaks move, and totals end up on the wrong row.

  • With <Printable>: nothing to do. It sizes itself to pageContentWidth(options).
  • Without <Printable>: set your container width explicitly:
import { pageContentWidth } from "react-print-pdf";

const options = { paperSize: "A4", margins: { /* … */ } };
const widthPx = pageContentWidth(options); // PDF points = px for our purposes

<div ref={ref} style={{ width: `${widthPx}px` }}>
  <YourDoc />
</div>

Keep fonts on the same axis as the screen

Fonts must be registered before export so they can be subset into the PDF:

import { registerFont } from "react-print-pdf";

await registerFont({
  family: "Inter",
  url: "/fonts/Inter-Regular.woff2",
  weight: 400,
});
await registerFont({
  family: "Inter",
  url: "/fonts/Inter-Bold.woff2",
  weight: 700,
});

Rules of thumb that prevent the most common font issues:

  • Use the exact same font on screen as in the export. Register the file, load it via @font-face for the screen, and the export uses the registered bytes — no second download.
  • Don't lazy-load fonts on the export click. Register at page mount so first export feels instant.
  • `font-weight: 400 and 700 are not free siblings. Register every weight you reference. Browsers fake intermediate weights; PDFs can't.
  • Skip variable fonts for v1. Pin to a static .woff2 or .ttf until variable font support lands.

See Fonts for the full registration API.

Pick the right "vector vs raster" boundary

You're renderingMake it vectorTag as raster
Body text, headings, paragraphsAlways
Tables, rules, separatorsAlways
Solid colored shapesAlways
Border-radius with solid bordersAlways
HyperlinksAlways (auto)
Photos, screenshots<img> (auto-embedded)
Logos with gradientsWrap in <div data-pdf-raster="true">
Shadow under a hero cardAuto-detected
SVG icon setMost stay vector via <img src="icon.svg"> (auto-embedded if image)Inline <svg> → automatic raster
Charts (recharts, victory, …)Auto-detected (inline SVG)

When in doubt: render to PDF, open in a reader, try to select the text. If you can, it's vector. If you can't, it's raster.

Pagination patterns that work

Pass them in options. They render on every page, including the first:

exportToPDF(ref.current, {
  header: <InvoiceHeader logoUrl="/logo.svg" docNumber="INV-2024-001" />,
  footer: ({ pageNumber, pageCount }) => (
    <div className="flex justify-between text-xs">
      <span>© Acme Inc.</span>
      <span>Page {pageNumber} of {pageCount}</span>
    </div>
  ),
  headerHeight: 56,
  footerHeight: 32,
});

Force-break before a new section

<section style={{ breakBefore: "page" }}>
  <h2>Appendix A — Terms & Conditions</h2>

</section>

Or with the data attribute (more reliable across CSS preprocessors):

<section data-pdf-break-before="page">…</section>

Keep a block together

Signature lines, KPI cards, and table rows you really don't want split:

<div style={{ breakInside: "avoid" }}>…</div>

"Bands" that span the full page width

For invoice totals, repeating dividers, or full-bleed colored bars under content — see Pagination → repeat bands.

Things to avoid

The list of things to avoid is short and specific. None of these are bugs — they're design choices we made on purpose.

Don't render the export target inside display: none

display: none strips boxes from layout. We can't read what isn't there. Use one of:

  • visibility: hidden + position: absolute (boxes still laid out)
  • An off-screen wrapper: position: fixed; left: -9999px; top: 0
  • <Printable> (does the right thing for you)

Don't expect transforms on the export root

We capture the DOM in its laid-out coordinate space. A transform: scale(0.5) on the root makes the export half-size, not "high-DPI 2× sharpness." If you need a higher-DPI raster fallback, set rasterPixelRatio in options instead.

Don't await the export inside an event handler that updates state synchronously

exportToPDF and usePDFExport().exportPDF() are async. They walk the DOM, wait for fonts, capture rasters, and serialize bytes. That can take 200ms–2s for a long document. Use the hook's isExporting state to disable the button instead of blocking the click handler.

<ExportButton target={ref}>Download</ExportButton>
// already handles disabled + loading state

Don't put state-driven UI inside the printable subtree mid-export

If your printable child reads from state that changes during export (timestamps, "now" clocks, animating numbers), the captured snapshot is whatever happened to be on screen the moment we walked. Freeze the data before calling exportPDF if it matters.

Don't try to print a virtualized list

Virtualized lists only render the rows currently in the viewport. The DOM doesn't contain the other 9,950 rows, so neither does the PDF. Render the full list (or paginate at the data layer) into the printable subtree before exporting.

Performance notes

react-print-pdf is fast for human-scale documents (1–50 pages) and acceptable for batches up to ~200 pages in one go. If you're past that, design accordingly:

  • Subset fonts early. Register every font once, at app boot. Re-registration costs.
  • Reuse the registered font registry across exports. It's a module-level cache; no extra work needed if you don't tear it down.
  • For a long report, the wall-clock cost is dominated by raster captures, not by PDF serialization. Minimize the number of raster regions (shadows, filters, charts) per page.
  • For multi-document batch exports (e.g. 100 invoices), export sequentially with a small await between calls so the main thread can paint progress UI. Parallelism inside one tab isn't worth it.

See Production checklist for the pre-launch sanity sweep.

When to escape to raster

Sometimes the right answer is "screenshot this one thing." Tag the smallest possible subtree:

<div data-pdf-raster="true">
  <ChartThatUsesCanvasAndGradients />
</div>

We capture exactly that <div> as a high-DPI PNG and emit it as a PDF image. The rest of the page stays vector. The default DPI is 2×; bump it via rasterPixelRatio: 3 if a designer is unhappy with sharpness.

Where to next

On this page