PDFreact-print-pdf
DocsBrowser-only alpha

How it works

A guided tour through the export pipeline, page geometry, and pagination — with diagrams.

Edit this page

This is the page to read once you've shipped a hello-world PDF and want to understand what's happening between "click button" and "PDF downloads." Skip it if you just need API surface — the API reference and export-to-pdf pages cover that.

The five-stage pipeline

The five stages from React render to PDF bytes
The five stages from React render to PDF bytesReact<Invoice />renders normallylive DOMrects + computedstyles + fontswalktext · bordersimages · rasterpaginateslice by pagerepeat header / footerPDFbytes(download)1. you write2. browser paints3. we capture4. split pages5. emitthe browser is the layout engine
The browser stays the layout engine. react-print-pdf reads what it painted, walks every box, and emits PDF primitives via pdf-lib.

Each stage is small and replaceable. We didn't write a layout engine. We didn't write a font rasterizer. We didn't write a PDF byte serializer. We wrote the glue between them.

1. You render

You write a regular React component. There's no special <Document> or <Page> wrapper to learn. Use Tailwind, use CSS modules, use styled-components, use design tokens. Whatever you already use for the screen.

function Invoice({ data }: { data: InvoiceData }) {
  return (
    <article className="bg-white p-12 text-slate-900">
      <header className="border-b pb-6">…</header>
      <table className="w-full">…</table>
      <footer className="mt-10">…</footer>
    </article>
  );
}

If it renders on screen, we can probably export it. If it doesn't render on screen, mount it in a hidden container — that's exactly what <Printable> does.

2. The browser paints

This is the part we don't write. The browser already knows how to:

  • compute styles cascaded across every selector you have
  • measure font metrics and line-wrap paragraphs
  • lay out tables and flex and grid
  • shape text in the user's locale

Asking JavaScript to redo this work is how rendering pipelines balloon. We take what the browser already produced.

3. We walk the DOM

We traverse from the root element you handed us, and for each node we record:

  • text runs: every line of text and exactly where its rectangle is, via Range.getClientRects()
  • boxes: backgrounds, borders, border-radius, per-side colors
  • images: the real bytes of <img> sources (decoded to PNG/JPEG for embedding)
  • raster regions: anything with a gradient, shadow, filter, transform, or an explicit data-pdf-raster="true" tag — captured as a high-DPI PNG of just that subtree
  • links: every <a href> becomes a click target rectangle

The output of this stage is a flat list of primitives in coordinate space, plus the raster captures.

4. We paginate

Now we have an arbitrarily tall captured tree and a finite page size. The paginator:

  • knows the content area of each page (paper size minus margins minus header/footer reserves)
  • walks the primitives top-to-bottom, slicing on safe boundaries when one wouldn't fit
  • re-renders the header at the top and footer at the bottom of every page
  • honors any break-before, break-after, or break-inside: avoid hints you placed

The next two diagrams make this concrete.

Page geometry

How margins and header / footer reserves shape one page
How margins and header / footer reserves shape one pageheader (repeats per page)footer · "Page 1 of 3"content areaeverything else you render(line items, paragraphs, tables, …)top marginbottom marginleft marginright marginA4 / Letter / customyou pick paper + orientationcontent = paper − margins − (header? + footer?)
Total paper minus four margins minus the optional header and footer reserves equals content area. That's the rectangle your component is asked to fill per page.

Concretely: an A4 page is 595 × 842 PDF points. If you set margins: { top: 40, right: 48, bottom: 40, left: 48 } and add a 56pt header reserve plus a 32pt footer reserve, the content area each page is asked to fill is 499 × 674 points (the rest is reserved for paper edges, header, and footer).

You don't have to compute that yourself. <Printable> does it for you and sizes its child to the content area's width so what you see on screen matches what comes out in the PDF.

If you're using exportToPDF directly without <Printable>, set your container width to pageContentWidth(options) and you'll get the same alignment.

Page breaks

Tables, repeating sections, and reports rarely fit on one page. The library handles overflow by slicing on safe boundaries.

When a table overflows, the paginator splits on row boundaries
When a table overflows, the paginator splits on row boundaries1 component → 10 rowsItem · Qty · Totalrow 1row 2row 3row 4row 5row 6row 7row 8row 9row 10paginateItem · Qty · Totalrow 1row 2row 3row 4row 5Page 1 of 2Item · Qty · Totalrow 6row 7row 8row 9row 10Page 2 of 2three nudges you controlbreak-beforeforce the next page to start here (cover, chapter, summary).break-afterfinish this block, then close the page.break-inside: avoidkeep this card / row / signature block together — push to next page if it would split.We never split an individual row. We may split the page between rows.
We slice on the next row that doesn't fit, then re-render the table header on the new page. Use break-before, break-after, or break-inside: avoid to nudge the boundary.

The default behavior is rarely surprising:

  • We never split an individual table row, list item, or text line down the middle.
  • Headers and footers repeat automatically — you don't need to copy them per page.
  • If a block doesn't fit at the current y-position but fits at the top of the next page, we push it to the next page.

When the default isn't enough, you have three nudges:

HintWhen to reach for it
break-before (CSS) or data-pdf-break-beforeForce a chapter / cover / summary to start on a fresh page.
break-after (CSS) or data-pdf-break-afterFinish a block, then close the page (e.g. the section before the appendix).
break-inside: avoid or data-pdf-keep-togetherKeep a signature block / KPI card / receipt total together — push to next page rather than split.

See Pagination for the full reference and edge cases (tall single elements, conflicting hints, manual page numbers).

Why the design ended up this way

Three decisions shaped the whole library. Mention any of them if you ever decide to fork or extend.

  1. The browser is the layout engine. We refused to write our own text shaper, table layout, or flex/grid solver. There are too many edge cases and the browser already solved them. Our job is to read what the browser produced.

  2. Vector first, raster as a graceful fallback. Selectable text is the single feature users care about most in a PDF. Everything that can be vector, is vector. Effects that the browser draws via GPU shaders (gradients, shadows, filters) get captured as PNG so the PDF doesn't lie about what the screen looks like — but only those regions, not the whole page.

  3. No new component language. We considered an <PdfDocument><PdfPage>… API. We didn't ship it because every team we talked to already has a working document component and didn't want a parallel codebase that drifts.

Where to next

On this page