Best practices
Patterns that ship, anti-patterns that bite, and the small rules that keep PDF output identical to what you see on screen.
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
reffor the export hook - adds a
data-rpp-printablemarker 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 topageContentWidth(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-facefor 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
.woff2or.ttfuntil variable font support lands.
See Fonts for the full registration API.
Pick the right "vector vs raster" boundary
| You're rendering | Make it vector | Tag as raster |
|---|---|---|
| Body text, headings, paragraphs | Always | — |
| Tables, rules, separators | Always | — |
| Solid colored shapes | Always | — |
| Border-radius with solid borders | Always | — |
| Hyperlinks | Always (auto) | — |
| Photos, screenshots | <img> (auto-embedded) | — |
| Logos with gradients | — | Wrap in <div data-pdf-raster="true"> |
| Shadow under a hero card | — | Auto-detected |
| SVG icon set | Most 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
Repeating header and footer
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 stateDon'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
awaitbetween 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
- How it works — pipeline tour with diagrams
- Examples — copy-paste-modify real-world docs
- Pagination — the full break / repeat reference
- Limitations — the short, honest list
- Production checklist — pre-launch sweep