How it works
A guided tour through the export pipeline, page geometry, and pagination — with diagrams.
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
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, orbreak-inside: avoidhints you placed
The next two diagrams make this concrete.
Page geometry
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.
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:
| Hint | When to reach for it |
|---|---|
break-before (CSS) or data-pdf-break-before | Force a chapter / cover / summary to start on a fresh page. |
break-after (CSS) or data-pdf-break-after | Finish a block, then close the page (e.g. the section before the appendix). |
break-inside: avoid or data-pdf-keep-together | Keep 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.
-
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.
-
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.
-
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
- Quick start — the 3-minute build
- Examples — real-world business documents with preview + code
- Pagination — break hints, repeat bands, edge cases
- Best practices — what to do and what to avoid
- Limitations — the short, honest list