# react-print-pdf — full documentation Browser-side React-to-vector-PDF library. This file contains the entire docs in one place for AI ingestion. Generated automatically — do not edit by hand. Canonical site: https://react-print-pdf-inky.vercel.app/docs/introduction Source repository: https://github.com/hempun10/react-print-pdf --- ## AI guide (Cursor, Claude Code, Copilot) Source: https://react-print-pdf-inky.vercel.app/docs/ai-guide > Drop-in instructions, system prompts, and rules so AI coding tools generate correct react-print-pdf code on the first try. If your team uses Cursor, Windsurf, Claude Code, GitHub Copilot, or any LLM-backed IDE, this page is the cheat sheet to feed those tools so they produce idiomatic `react-print-pdf` code without dragging in bad patterns from screenshot libraries or PDF renderers with a different model. ## Machine-readable docs Two files for any AI crawler: | File | Purpose | | --- | --- | | [`/llms.txt`](/llms.txt) | The lightweight RFC-format index. Lists every doc page and its purpose. | | [`/llms-full.txt`](/llms-full.txt) | The entire docs concatenated into one file (~100 kB) for full-context ingestion. | Both update on every docs deploy. The full file is generated automatically from the `.mdx` source — never hand-edited, never stale. Any agent that respects the [llmstxt.org](https://llmstxt.org) convention will discover both. For tools that don't, point them at the full file directly: ``` https://react-print-pdf-inky.vercel.app/llms-full.txt ``` ## One-paragraph system prompt Copy this into your tool's system prompt, project rules, or `.cursorrules` block: ``` You are helping build PDF export features with react-print-pdf 0.2 — a browser-side library that turns a live DOM tree into a vector PDF via pdf-lib. Render normal React components, then either call exportToPDF(element, options) imperatively OR use the React DX layer: usePDFExport() returns { ref, exportPDF, isExporting }; wrap printable content in and trigger from . Match on-screen container width to pageContentWidth(options) so layout never reflows between screen and PDF. Register custom fonts via registerFont() before export; expect Courier and other built-ins to be limited to WinAnsi. Pagination is automatic: line items split on row boundaries; use break-before / break-after / break-inside: avoid to nudge. Gradients, shadows, transforms, filters, and inline SVG auto-rasterize; anything tagged data-pdf-raster='true' also rasterizes. The library is browser-only for v1; do not propose Node or SSR usage. ``` ## Tool-specific rules ### Cursor (`.cursorrules` or Project Rules) Drop this file at the repo root or under `.cursor/rules/react-print-pdf.md`: ````md --- description: react-print-pdf usage rules globs: ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"] --- # react-print-pdf You are working in a project that uses react-print-pdf 0.2 to export React components to vector PDFs in the browser. ## API surface (memorize) - `exportToPDF(element: HTMLElement, options?: ExportOptions): Promise` - `usePDFExport(options): { ref, exportPDF, isExporting, error, lastResult, reset }` - `...` - `Label` - `registerFont({ family, url, weight?, style? }): Promise` - `pageContentWidth(options): number` - `PdfExportError(code, message, options?)` — code is FIRST ## Rules 1. Prefer the React DX layer (`usePDFExport` + `` + ``) over raw `exportToPDF` unless the user needs to export DOM outside their component tree. 2. Match container width to `pageContentWidth(options)` so screen layout matches PDF layout. `` does this for free. 3. Register fonts at module load or app boot, never on the export click. 4. Do NOT propose server-side usage — this is browser-only for v1. 5. Do NOT propose `display: none` for hidden printable content. Use `` (mounted off-screen by default), `visibility: hidden + position: absolute`, or a fixed-position off-screen container. 6. Pagination automatically slices on row boundaries. To force a page break, use `style={{ breakBefore: 'page' }}` or `data-pdf-break-before="page"`. To keep content together, use `style={{ breakInside: 'avoid' }}` or `data-pdf-keep-together`. 7. Inline ``, gradients, shadows, CSS filters, and CSS transforms automatically rasterize. To force raster on any subtree, wrap with `
`. 8. Tables that paginate should mark their header with `` so the header repeats on continuation pages. 9. For repeating page headers and footers, pass `header` and `footer` as functions of `PageContext` (`{ pageNumber, totalPages }`) in the export options. Reserve their vertical space with `headerHeight` and `footerHeight`. 10. Errors throw `PdfExportError(code, message)`. Common codes: `INVALID_ELEMENT`, `FONT_REGISTRATION_FAILED`, `EXPORT_ABORTED`. Always show the code to the user; don't swallow. ## Anti-patterns - ❌ `exportToPDF(, ...)` — pass an HTMLElement, not a React element. - ❌ `` as the printable target — boxes aren't laid out. - ❌ Loading fonts inside the export handler — register at mount. - ❌ Wrapping a chart and 200 surrounding pixels in `data-pdf-raster` — tag the smallest possible subtree. - ❌ Calling `exportToPDF` synchronously from a render — it's async and must be awaited from an event handler. - ❌ Virtualized lists as printable content — only the rendered window makes it into the PDF. ## Full reference Always link the user to https://react-print-pdf-inky.vercel.app/llms-full.txt when writing custom code beyond these patterns. ```` ### Claude Code (`CLAUDE.md` at repo root) ````md # Project rules We use react-print-pdf 0.2 for browser-side PDF export. Follow these rules: ## Generation patterns Prefer the React DX layer. The canonical export pattern: ```tsx import { usePDFExport, Printable, ExportButton } from "react-print-pdf/react"; function ExportableDoc({ data }) { const exporter = usePDFExport({ fileName: "doc.pdf", paperSize: "A4", margins: { top: 40, right: 48, bottom: 40, left: 48 }, }); return ( <> Download ); } ``` When generating custom paper sizes (e.g. thermal receipts), use a `[width_mm, height_mm]` tuple as the paperSize. When generating a multi-page table, mark the header ``. When generating headers and footers with page numbers, pass functions of `PageContext`: ```tsx header: ({ pageNumber, totalPages }) =>
, headerHeight: 40, ```` ## Things to never do - Never use `display: none` for the printable element. - Never call `exportToPDF` in a render function. - Never propose a Node-side or SSR usage. - Never put a virtualized list inside `` and expect all rows to make it. - Never wrap an entire page in `data-pdf-raster` — tag the smallest subtree that needs it. ## Reference Full docs at https://react-print-pdf-inky.vercel.app/docs/introduction. Machine-readable: https://react-print-pdf-inky.vercel.app/llms-full.txt ``` ### Windsurf (`.windsurfrules`) Use the same content as the Cursor file. Windsurf reads the same Markdown rules; symlink or duplicate. ### GitHub Copilot Copilot doesn't accept arbitrary rules files yet. Workarounds: 1. **JSDoc comments in your wrapper component.** Copilot picks up nearby comments as context: ```tsx /** * Wrap any component in to make it PDF-exportable. * Usage: useDOMExport pattern with usePDFExport + ExportButton. * See https://react-print-pdf-inky.vercel.app/docs/quick-start. * * Never use display:none for the inner content. * Always register fonts at boot, not on click. */ ``` 2. **A `docs/pdf-export.md` in your repo** that Copilot indexes alongside source. Copy the Cursor rules content there. 3. **`.github/copilot-instructions.md`** at repo root. Copilot reads this file globally for the org (if your org has the feature enabled). ## Asking the right questions When you prompt an AI agent for code, framing matters. Some templates that work well: | Goal | Prompt template | | --- | --- | | New document type | "Using react-print-pdf 0.2, build a `` that paginates across A4 with a repeating header containing ``." | | Bug fixing | "I'm seeing ``. I render with `` and call `exporter.exportPDF()`. Diagnose using the limitations page at https://react-print-pdf-inky.vercel.app/docs/limitations." | | Font registration | "Register the `` font at all weights I use (``). Show the registration code and the order of operations vs the export call." | | Custom paper size | "Generate the export call for a ``mm × ``mm receipt, including the on-screen container width to match." | ## Common mistakes AI agents make What to watch for when reviewing AI-generated code: | Symptom | Cause | Fix | | --- | --- | --- | | `exportToPDF` called with a React element, not a DOM element | The AI confused this with `@react-pdf/renderer` | Pass a ref's `.current`, or use `` | | Headers and footers as static JSX instead of functions | The AI didn't realize they receive `PageContext` | `header: ({ pageNumber }) => ...` | | `display: none` on the printable target | Confusion with PDF libraries that read source code | Use `` (handles off-screen mounting) or `visibility: hidden + absolute` | | Imports from `react-print-pdf/server` | The AI hallucinated a server entry | There isn't one for v1. Imports from `react-print-pdf` and `react-print-pdf/react`. | | `register Font` mid-render | Trying to lazy-load | Register at module top or in a `useEffect` on app mount | | Chart wrapped in `data-pdf-raster` AND containing parent div also tagged | Over-tagging | Only the chart wrapper needs it | ## Pin a version in your prompt LLMs trained earlier may know about a different API surface from a prior alpha. Always include the version in your prompt: ``` We're on react-print-pdf 0.2.0 stable. The current export contract is exportToPDF(element, options) and the React DX layer ships usePDFExport + + . ``` ## Where to next - [llms.txt](/llms.txt) — RFC-format index for AI crawlers - [llms-full.txt](/llms-full.txt) — full docs concatenated - [Best practices](/docs/best-practices) — what to do and avoid in production code - [Limitations](/docs/limitations) — the short, honest list --- ## API reference Source: https://react-print-pdf-inky.vercel.app/docs/api-reference > The current public exports and TypeScript types. ## Functions ### `exportToPDF(element, options?)` ```ts function exportToPDF( element: HTMLElement, options?: ExportOptions, ): Promise; ``` Exports a mounted DOM element to PDF. ### `registerFont(descriptor)` ```ts function registerFont(descriptor: FontDescriptor): Promise; ``` Loads a TTF/OTF font into the browser via `FontFace`, caches the bytes, and makes it available for PDF embedding. ### `getRegisteredFonts()` ```ts function getRegisteredFonts(): ReadonlyMap; ``` Returns the current font registry. Mainly useful for diagnostics and tests. ### `clearFontRegistry()` ```ts function clearFontRegistry(): void; ``` Clears registered fonts and removes matching `FontFace` entries from `document.fonts` when possible. Primarily a test helper. ### `pageContentWidth(opts?)` ```ts function pageContentWidth(opts?: PageGeometryOptions): number; ``` Returns the **CSS pixel** width of the printable area for the given format / orientation / margin. Use this to size your root exportable container so it fills the page without slack. ```tsx import { exportToPDF, pageContentWidth } from "react-print-pdf"; const width = pageContentWidth({ format: "A4", margin: "10mm" }); // 718 return (
); ``` Defaults: `format: "A4"`, `orientation: "portrait"`, `margin: 0`. ### `pageContentHeight(opts?)` ```ts function pageContentHeight(opts?: PageGeometryOptions): number; ``` Like `pageContentWidth` but for height. Useful when authoring `pageBreak: "single"` documents that must fit one page. ## Components ### `` ```tsx import { PageBreak } from "react-print-pdf"; ``` Forces a page boundary in `auto` and `manual` pagination modes. ## Constants ### `PAGE_BREAK_ATTR` ```ts PAGE_BREAK_ATTR === "data-print-page-break"; ``` The DOM marker used by ``. ### `PRINT_REPEAT_ATTR` ```ts PRINT_REPEAT_ATTR === "data-print-repeat"; ``` Marks a DOM region that should repeat at the top of continuation pages. ## Errors ### `PdfExportError` ```ts class PdfExportError extends Error { readonly code: PdfExportErrorCode; readonly context?: PdfExportErrorContext; } ``` Thrown by `exportToPDF` and `registerFont` for documented failure modes. The `code` field is stable and safe to switch on; the `message` is a human-readable summary that may change between releases. See [Error handling](/docs/error-handling) for the full catalogue and recovery patterns. ### `isPdfExportError(err)` ```ts function isPdfExportError(err: unknown): err is PdfExportError; ``` Type guard for use in `catch` blocks. ## Debug overlay Passing `debug: true` to `exportToPDF` draws a thin colored outline around every emitted primitive's bounding box plus a small label in the top-left corner indicating the primitive kind. The overlay is drawn on top of the content so it sits above everything else, which is what you want when chasing a misaligned element or verifying that a region rasterized vs. drew vector. ```ts await exportToPDF(ref.current, { filename: "debug.pdf", debug: true, }); ``` Color key: - blue — `rect` - magenta — `border` - red — `text` - green — `image` - olive — `raster` (rasterized region) - orange — `link` (one box per visual line) - teal — `softRect` / `softRectSlice` This is a development aid; ship `debug: false` (the default) to your users. ## Lifecycle hooks ### `onProgress(stage, pct)` Reports coarse pipeline progress. The five stages run in order: `prepare → walk → paginate → emit → deliver`. Each stage fires `0` at start and `1` at end. ### `onPageRendered(pageNumber, totalPages)` Fires once per page after the page is fully composed but before the document is serialized. Pages report in order, starting at `1`. The `totalPages` value is consistent across calls. Throwing from this hook propagates to the caller of `exportToPDF` and aborts the export. ```ts await exportToPDF(ref.current, { onPageRendered: (pageNumber, total) => { console.log(`rendered ${pageNumber}/${total}`); }, }); ``` ## Hyperlinks Any `` inside the exportable subtree becomes a clickable PDF Link annotation when the URI uses an allowed scheme (`http`, `https`, `mailto`, `tel`). The walker emits one annotation per visual line via `el.getClientRects()` so wrapped links stay clickable across multiple lines. Internal anchors (`href="#section"`) and unsafe schemes (`javascript:`, `data:`, `blob:`, protocol-relative `//`, empty) are dropped at walk time — the visible text and underline still render. There is no opt-out flag; if you want a non-clickable styled link, render a `` with the same classes. ## Types ```ts type PageFormat = "A4" | "A3" | "A5" | "Letter" | "Legal" | [number, number]; type Orientation = "portrait" | "landscape"; type Margin = | string | number | { top: string | number; right: string | number; bottom: string | number; left: string | number; }; type PageBreakMode = "auto" | "manual" | "single"; type PageContext = { pageNumber: number; totalPages: number; }; type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; type FontStyle = "normal" | "italic"; type Align = "left" | "center" | "right"; type PageGeometryOptions = { format?: PageFormat; orientation?: Orientation; margin?: Margin; }; type FontDescriptor = { family: string; src: string; weight?: FontWeight; style?: FontStyle; }; type RegisteredFont = Required & { bytes: ArrayBuffer; }; type ExportOptions = { filename?: string; format?: PageFormat; orientation?: Orientation; margin?: Margin; align?: Align; pageBreak?: PageBreakMode; header?: (ctx: PageContext) => ReactElement; footer?: (ctx: PageContext) => ReactElement; title?: string; author?: string; onProgress?: (stage: ExportStage, pct: number) => void; onPageRendered?: (pageNumber: number, totalPages: number) => void; debug?: boolean; }; type ExportStage = "prepare" | "walk" | "paginate" | "emit" | "deliver"; type PDFExportResult = { bytes: () => Uint8Array; blob: () => Blob; save: (filename?: string) => void; preview: () => Window | null; }; type PdfExportErrorCode = | "INVALID_ELEMENT" | "FONT_FETCH_FAILED" | "FONT_INVALID_BYTES" | "FONT_LOAD_FAILED" | "RENDER_DETACHED"; type PdfExportErrorContext = { family?: string; src?: string; status?: number; cause?: unknown; }; ``` --- ## Architecture Source: https://react-print-pdf-inky.vercel.app/docs/architecture > The browser is the layout engine; react-print-pdf is the capture, pagination, and PDF emit layer. `react-print-pdf` intentionally avoids a custom layout engine. The browser already knows how your React document should look, so the library reads the browser's result. ## Pipeline ```txt prepare wait for fonts measure headers/footers rasterize fallback regions walk read element boxes read computed styles read text client rects collect images and repeat bands paginate resolve page size and margins slice primitives into pages place headers, footers, and repeat bands emit embed fonts and images draw primitives through pdf-lib save bytes deliver expose bytes(), blob(), save(), preview() ``` ## Why the API takes an HTMLElement The exporter needs the real DOM node because layout has already happened there: ```tsx const result = await exportToPDF(printRef.current, options); ``` Passing JSX directly would require the library to mount and style a second tree. That is possible later, but the current API keeps the boundary explicit: **you render; the library captures**. ## Coordinate system The walker records DOM coordinates in CSS pixels. The emitter converts them to PDF points using: ```ts const PX_TO_PT = 72 / 96; ``` This same conversion is used for positions and font sizes so geometry stays internally consistent. ## Vector primitives The walker emits a small internal primitive set: | Primitive | Source | | --- | --- | | `rect` | solid backgrounds | | `border` | computed border widths/colors | | `text` | text node ranges | | `image` | PNG/JPEG `` sources | | `raster` | fallback PNG snapshots | | `pageBreak` | `` markers | | `groupStart/groupEnd` | `break-inside: avoid` ranges | PDF output is not a browser print screenshot. Most text and simple geometry stays vector. ## Async boundaries The DOM walker is synchronous. Anything that requires I/O happens before or after the walk: - font registration happens before export - raster fallback targets are captured in a prepare pass - image bytes are fetched and embedded before drawing That keeps the walker deterministic and makes failures easier to isolate. ## Browser-only v1 Server-side PDF generation is out of scope for v1 because the core value comes from browser layout and computed styles. A future server mode would need a real DOM implementation plus font/layout parity work. --- ## Best practices Source: https://react-print-pdf-inky.vercel.app/docs/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](/docs/api-reference); this is the "what should I actually do" version. ## Use `` unless you have a reason not to The `` 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 ```tsx 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 ( <> Download PDF ); } ``` 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 ``: nothing to do. It sizes itself to `pageContentWidth(options)`. - Without ``: set your container width explicitly: ```tsx import { pageContentWidth } from "react-print-pdf"; const options = { paperSize: "A4", margins: { /* … */ } }; const widthPx = pageContentWidth(options); // PDF points = px for our purposes
``` ## Keep fonts on the same axis as the screen Fonts must be **registered before export** so they can be subset into the PDF: ```tsx 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](/docs/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 | `` (auto-embedded) | — | | Logos with gradients | — | Wrap in `
` | | Shadow under a hero card | — | Auto-detected | | SVG icon set | Most stay vector via `` (auto-embedded if image) | Inline `` → 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: ```tsx exportToPDF(ref.current, { header: , footer: ({ pageNumber, pageCount }) => (
© Acme Inc. Page {pageNumber} of {pageCount}
), headerHeight: 56, footerHeight: 32, }); ``` ### Force-break before a new section ```tsx

Appendix A — Terms & Conditions

``` Or with the data attribute (more reliable across CSS preprocessors): ```tsx
``` ### Keep a block together Signature lines, KPI cards, and table rows you really don't want split: ```tsx
``` ### "Bands" that span the full page width For invoice totals, repeating dividers, or full-bleed colored bars under content — see [Pagination → repeat bands](/docs/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` - `` (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. ```tsx Download // 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](/docs/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: ```tsx
``` We capture exactly that `
` 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](/docs/how-it-works) — pipeline tour with diagrams - [Examples](/docs/examples/receipt) — copy-paste-modify real-world docs - [Pagination](/docs/pagination) — the full break / repeat reference - [Limitations](/docs/limitations) — the short, honest list - [Production checklist](/docs/production-checklist) — pre-launch sweep --- ## Error handling Source: https://react-print-pdf-inky.vercel.app/docs/error-handling > Typed errors thrown by exportToPDF and registerFont, with stable codes you can branch on. `react-print-pdf` throws a custom `PdfExportError` from every public API failure. The error has a stable `code` you can switch on without parsing the message. ## The `PdfExportError` shape ```ts import { PdfExportError, isPdfExportError } from "react-print-pdf"; class PdfExportError extends Error { readonly code: PdfExportErrorCode; readonly context: PdfExportErrorContext; // ES2022 standard `cause` field — set when an underlying error // (fetch, FontFace.load) was wrapped. readonly cause?: unknown; } ``` Use `isPdfExportError(value)` as a type guard inside `catch` blocks. ## Codes | Code | Where | Why it fires | |---|---|---| | `INVALID_ELEMENT` | `exportToPDF` | The element argument was `null` or `undefined`. Usually a missing `ref.current`. | | `FONT_FETCH_FAILED` | `registerFont` | The font URL returned a non-2xx HTTP status. Check the URL is reachable and CORS-allowed. | | `FONT_INVALID_BYTES` | `registerFont` | The response body was shorter than 100 bytes. Almost always means the server sent an HTML 404 page or a redirect, not a font file. | | `FONT_LOAD_FAILED` | `registerFont` | The browser's `FontFace.load()` rejected the bytes. Common cause: the URL points at WOFF/WOFF2, which `pdf-lib` cannot embed. Use TTF or OTF. | | `RENDER_DETACHED` | internal (header / footer rerender) | The hidden React root was unmounted before `rerender` was called. Should not happen during a normal export. | Codes are stable across patch releases. New codes may be added in minor releases. Renames are breaking changes. Image-fetch and raster-fallback failures intentionally do **not** throw — they print a `console.warn` and the export continues with the failed asset replaced. Promoting them to errors would break documents that already tolerate lossy export. A future opt-in `onError` callback in `ExportOptions` will let you escalate these to thrown errors per-call. ## Branching example ```tsx import { exportToPDF, isPdfExportError } from "react-print-pdf"; async function handleExport(el: HTMLElement) { try { const { blob } = await exportToPDF(el, { filename: "report.pdf" }); download(blob); } catch (e) { if (isPdfExportError(e)) { switch (e.code) { case "INVALID_ELEMENT": toast.error("The export target hasn't mounted yet — try again in a moment."); break; case "FONT_FETCH_FAILED": case "FONT_INVALID_BYTES": case "FONT_LOAD_FAILED": // Log structured context for the bug tracker. reportToSentry({ code: e.code, ...e.context, cause: e.cause }); toast.error("Failed to load a custom font — falling back to defaults."); break; default: toast.error("Export failed. The team has been notified."); reportToSentry({ code: e.code, message: e.message }); } return; } // Non-PdfExportError (probably a pdf-lib bug or unhandled DOM error). reportToSentry(e); toast.error("Unexpected error."); } } ``` ## Diagnostic context `error.context` carries free-form fields useful for logs and bug reports. They are **not** part of the public branching contract — fields may appear or disappear between minor releases. | Field | Type | Set by | |---|---|---| | `url` | `string` | `FONT_*` | | `status` | `number` | `FONT_FETCH_FAILED` | | `byteLength` | `number` | `FONT_INVALID_BYTES`, `FONT_LOAD_FAILED` | | `detail` | `string` | `FONT_LOAD_FAILED` (underlying browser error message) | Always log `error.code`, `error.message`, `error.context`, and `error.cause` together. That tuple is enough to reproduce 95% of the failure modes we've seen in the wild. --- ## Financial summary report Source: https://react-print-pdf-inky.vercel.app/docs/examples/financial-report > A quarterly summary with KPI cards, three charts (vector + raster fallback), and a transactions table that paginates with a repeating header. ## What you're looking at The report that lands in a board pack or an investor update. KPI cards across the top, three charts (revenue trend, top products bar, team-share donut), then a paginated transactions section. This is the example that exercises the **mixed vector / raster** strategy hardest: - **KPI cards.** Vector text, vector borders, vector backgrounds. The number you care about is selectable in the PDF. - **Recharts SVG charts.** Recharts renders inline `` which the walker captures as a high-DPI PNG raster. You get the visual fidelity without writing a PDF chart engine. - **Transactions table.** Vector text, vector borders, repeating column header across pages. - **Section dividers.** Plain `
` and Tailwind borders. Vector, sharp at any zoom. The boundary lands where it should: numbers stay selectable; pixel-perfect chart rendering becomes a high-DPI image. No invisible vector text painted underneath the chart trying to mimic axis labels (we considered it; it isn't worth the complexity). ## The exporter call The chart section needs slightly higher raster DPI than the default. Otherwise zooming into a printed copy reveals the chart edges as soft instead of crisp: ```tsx const exporter = usePDFExport({ fileName: `q2-summary-${year}.pdf`, paperSize: "A4", margins: { top: 56, right: 48, bottom: 48, left: 48 }, rasterPixelRatio: 3, // \u2190 charts at 3\u00d7 sharpness header: ({ pageNumber, totalPages }) => ( ), headerHeight: 40, }); ``` `rasterPixelRatio` only affects auto-raster regions (charts, shadows, transforms). It doesn't bloat vector content. ## The component [Full source on GitHub](https://github.com/hempun10/react-print-pdf/blob/main/examples/demo/src/DashboardFixture.tsx) \u2014 it's the longest fixture in the workshop because it actually demonstrates four sections of a real report. The shape: ```tsx export function FinancialSummary({ data }) { return (
{/* 1. KPI grid \u2014 four boxes across, vector text */}
{/* 2. Charts \u2014 inline SVG from recharts, auto-rasterized */}

Revenue trend

Top products

{topProducts.map((_, i) => )}
{/* 3. Commentary block \u2014 vector body text */}

Quarter commentary

Revenue grew 18% QoQ driven by enterprise upgrades \u2026

{/* 4. Transactions table \u2014 paginates with repeating header */}

Transactions \u2014 Q2 2026

{data.txns.map(t => )}
IDDateCustomerTypeAmount
); } ``` ## Why the choices look the way they do ### Three charts, three layouts A 12-column grid (`grid-cols-12`) with a 7/5 split for the two top charts, then a full-width donut row beneath. This mirrors what board packs actually use. Don't bother with a 4-column grid for charts \u2014 charts need real estate to be readable, especially after rasterization. ### `border` on chart wrappers, not `shadow` We deliberately use a flat `border border-gray-200` instead of a `shadow-sm` around each chart card. Shadows are a raster boundary \u2014 anything inside a shadow-wrapped card gets captured as one big PNG, not just the chart. Using a border keeps the **card title and chart** as separate vector / raster regions, and the title stays selectable in the PDF. ### KPI delta colors are vector Red for negative, green for positive, gray for neutral \u2014 all driven by Tailwind utility classes. The walker reads the computed color from each KPI card and emits vector text with that color. No raster needed. ### Commentary card has a `border border-gray-900` Bold border = "this is the part executives actually read." A subtle border or no border would lose the visual hierarchy in print. The commentary is *the report* \u2014 the numbers and charts are evidence. ## When NOT to use this shape If your report is **20+ pages of dense tables and footnotes**, this layout doesn't scale. KPI dashboards live at the front; the back half should be transactions + appendices. Use `break-before: page` to separate the dashboard front-half from the data-dense back-half so readers can flip the binder open at either. ## Variations - **Replace recharts with Visx, Victory, Chart.js, etc.** The library doesn't care which chart engine emits SVG. Any inline `` becomes a raster region automatically. - **Add commentary per chart.** Two-column row, chart on the left, commentary on the right. Keep the chart's wrapper `
` simple so the raster boundary stays tight. - **Brand colors.** Replace the `PALETTE` array. The library reads `fill` and `stroke` attributes from inline SVG when rasterizing, so brand colors carry through. - **Multi-quarter comparison.** Add a second `BarChart` underneath the revenue trend with `data={monthlyPnlComparison}`. Each chart is independent. ## Related - [Raster fallback](/docs/raster-fallback) \u2014 how charts get captured - [Best practices](/docs/best-practices) \u2014 picking the vector / raster boundary - [Pagination](/docs/pagination) \u2014 the transactions table behavior --- ## Examples Source: https://react-print-pdf-inky.vercel.app/docs/examples > Real-world business documents built with react-print-pdf. Live preview, full source, ready to copy. Four documents that real businesses send out every day. Each page has: - a **live preview** powered by the public workshop deployment - the **full source** for the component - a **commentary** explaining which library features the example exercises and why each pattern is there Click "Export PDF" in any preview to download a real, selectable, embedded-font PDF. ## Pick one | Document | Format | Notable features | | --- | --- | --- | | [Receipt (thermal 80mm)](/docs/examples/receipt) | 80mm thermal slip | Narrow page, monospace stack, modifier lines, GST split | | [Invoice (A4 multi-page)](/docs/examples/invoice) | A4 | Header / footer repeat, page-breaking line items, totals band | | [Statement of account](/docs/examples/statement) | A4 | 64 transactions, repeating table header, sticky closing balance | | [Financial summary report](/docs/examples/financial-report) | A4 | KPI grid, mixed charts (vector + raster fallback), commentary blocks | All four ship in the [workshop repo](https://github.com/hempun10/react-print-pdf/tree/main/examples/demo/src) under `examples/demo/src/`. Open one fixture and edit \u2014 the workshop's local dev server uses `file:../..` so library changes show up immediately. ## Copying patterns from these examples The examples are intentionally pragmatic, not minimal. They include: - realistic data shapes (line items, transactions, KPIs) - branded headers and footers - the small bits of CSS that matter for print (matched widths, `break-inside`, `data-pdf-raster`) So you can usually take a fixture, swap your own data in, and have something serviceable in 15 minutes. The full reference for each pattern is in [Best practices](/docs/best-practices) and [Pagination](/docs/pagination). --- ## Invoice (A4 multi-page) Source: https://react-print-pdf-inky.vercel.app/docs/examples/invoice > A B2B invoice with 50 line items that paginates across three A4 pages, with header / footer that repeat and a totals card that sits flush at the end. ## What you're looking at The invoice that every SaaS, agency, and consultancy ends up needing. 50 line items, three pages, header and footer that repeat on every page, and a totals card that stays glued to the end of the line items (not stranded on a page of its own). Features in play: - **Pagination.** Line items don't fit on one page, so the paginator splits the table at the next safe row boundary. - **Repeating header.** The doc header (invoice number, date, totals page indicator) renders at the top of every page automatically. You supply it once. - **Repeating footer.** "Page N of M" with the document footer text, also automatic. - **``.** The table's column headers (SKU / Description / Qty / Unit / Amount) repeat at the top of each new page so a reader on page 3 still knows which column is which. - **`break-inside: avoid` on the totals card.** Keeps Subtotal / Tax / Total together, never splits them mid-page. - **Page numbers in the header AND footer.** They get the current page context (`pageNumber`, `totalPages`) injected at render time. ## The exporter call This is where the React DX layer earns its keep \u2014 you don't compute page count, you don't loop, you don't reach for templates. You hand it a component and a header / footer: ```tsx import { usePDFExport, Printable, ExportButton } from "react-print-pdf/react"; import type { PageContext } from "react-print-pdf"; function InvoicePage({ invoice }) { const exporter = usePDFExport({ fileName: `invoice-${invoice.number}.pdf`, paperSize: "A4", margins: { top: 56, right: 48, bottom: 48, left: 48 }, header: ({ pageNumber, totalPages }: PageContext) => ( ), footer: ({ pageNumber, totalPages }: PageContext) => (

Generated by react-print-pdf \u00b7 page {pageNumber} / {totalPages}

), headerHeight: 40, footerHeight: 24, }); return ( <> Download invoice ); } ``` Three things to note: 1. The `header` and `footer` are **functions** of `PageContext`, not static elements. That's how you get page numbers without writing a paginator yourself. 2. `headerHeight` and `footerHeight` reserve the vertical space on every page. The paginator then knows the actual content area is `paperHeight - margin.top - margin.bottom - headerHeight - footerHeight`. 3. `Printable` auto-sizes its child to `pageContentWidth(options)` so the on-screen preview lays out exactly the way the PDF will. ## The component [Full source on GitHub](https://github.com/hempun10/react-print-pdf/blob/main/examples/demo/src/MultiPageInvoice.tsx). The shape: ```tsx export function Invoice({ data }) { const subtotal = data.items.reduce((s, it) => s + it.qty * it.unit, 0); const tax = subtotal * 0.08; const total = subtotal + tax; return (
{/* The line-items table. tells the paginator to re-render the column headers on every page. */}
{data.items.map((it) => ( ))}
SKUDescriptionQtyUnitAmount
{it.sku} {it.desc} {it.qty} {fmt(it.unit)} {fmt(it.qty * it.unit)}
{/* Totals card. break-inside: avoid keeps these three rows together regardless of where the table ends on the page. */}
); } ``` ## Why the choices look the way they do ### `` instead of CSS There is a CSS spec for this (`` is supposed to repeat on print). The reason we wrote a `data-` attribute instead of relying on CSS is that we needed an unambiguous signal in the walker. CSS print behavior varies enough between engines that depending on it for a library invariant turns out to bite. The data attribute is two characters longer and zero ambiguity. ### Totals card width pinned to `w-64` Because the totals card sits beside whitespace on the left (and `ml-auto` pushes it to the right), giving it a fixed width keeps the layout predictable when content changes. If your totals card has many more rows (per-tax-jurisdiction breakdown, surcharges), bump `w-64` to whatever fits. Don't let it flex \u2014 floating widths around `break-inside: avoid` is where surprises live. ### One footer string, not three The footer is intentionally just one centered string. Resist putting the company VAT number, payment terms, support email, and the SaaS marketing tagline all in the footer. That stuff belongs in a "terms" block on the last page (use `break-before: page` to push it onto its own page). A clean footer with page numbers reads as professional. ## Variations The fixture is single-currency, single-tax-rate. Common modifications: - **Multi-currency.** Replace `$` with a passed-in currency symbol; format with `Intl.NumberFormat` if you want locale-aware separators. - **Per-row discount.** Add a `discount` column and a discount-applied subtotal row. The totals card width may need to grow. - **Stamps and signatures.** A "Paid" or "Overdue" stamp goes inside `
` with `position: absolute; transform: rotate(-12deg)`. **Transforms force raster fallback** \u2014 that's usually what you want for a stamp anyway. - **Multiple tax rows.** Add rows to the totals card; the `break-inside: avoid` keeps them grouped. ## Related - [Pagination](/docs/pagination) \u2014 ``, break hints, repeat bands - [Best practices](/docs/best-practices) \u2014 width-matching, font registration - [Sizing and alignment](/docs/sizing-and-alignment) \u2014 `pageContentWidth(options)` --- ## Receipt (thermal 80mm) Source: https://react-print-pdf-inky.vercel.app/docs/examples/receipt > A restaurant slip printed at the table. Narrow page, monospace stack, modifier lines, GST split, "PAID" stamp. ## What you're looking at A complete thermal-printer receipt — the kind a Bistro / Bar / Cafe POS prints after you ask for the bill. It exercises the patterns that come up every time a team wants their "screen receipt" to also be a downloadable PDF: - **Custom paper size.** 80mm wide × 240mm tall. The library takes a `[width_mm, height_mm]` tuple, so anything off the A-series chart works the same way. - **Monospaced everything.** Body text is a real mono font (falls back to Courier). The library subsets and embeds the font into the PDF, so the receipt looks identical on every machine that opens it. - **Modifier lines.** Indented sub-rows under each item ("+ extra cheese", "+ no onion"). Vector text means the modifier names stay searchable in the PDF. - **GST split + service charge.** Three muted rows under the subtotal, one bold total. No special PDF logic — it's the same Tailwind utilities you'd use for the screen. - **"PAID" stamp.** A bordered inline-block with letter-spacing — vector, not raster. - **ASCII barcode strip in the footer.** Real text characters, so the user can copy them. (Swap for an `` if you want a scannable code.) ## The exporter call Two pieces. The first is the size preset: ```ts const RECEIPT_80MM = { format: [80, 240], // mm × mm, the library converts internally to points margin: "4mm", // small margins on a thermal slip kind: "thermal", }; ``` The second is the actual export, using the React DX layer: ```tsx import { usePDFExport, Printable, ExportButton } from "react-print-pdf/react"; function ReceiptPage({ order }) { const exporter = usePDFExport({ fileName: `receipt-${order.orderNo}.pdf`, paperSize: RECEIPT_80MM.format, margins: RECEIPT_80MM.margin, }); return ( <> Download receipt ); } ``` That's all the export plumbing. Everything else is the component itself. ## The component [Full source on GitHub](https://github.com/hempun10/react-print-pdf/blob/main/examples/demo/src/RestaurantReceiptFixture.tsx) — 164 lines, plain React + Tailwind. The structural skeleton: ```tsx export function RestaurantReceipt({ order }) { // 1. Compute totals from items + modifiers. let subtotal = 0; for (const it of order.items) { subtotal += it.qty * it.unit; for (const m of it.modifiers) subtotal += m.price; } const cgst = Math.round(subtotal * 0.025); const sgst = Math.round(subtotal * 0.025); const service = Math.round(subtotal * 0.05); const total = subtotal + cgst + sgst + service; return (
{/* business name, address, GSTIN */}
); } /** Horizontal rule drawn as a real 1px border. We don't print a row of * ASCII '-' or '─' characters because the chosen mono fallback (Courier) * doesn't always have the box-drawing glyph, and a CSS border stays sharp * at any export resolution. */ function Sep({ dashed = false }) { return (
); } ``` ## Why the choices look the way they do A couple of decisions in this fixture are deliberate, not stylistic. Worth calling out: ### Why `border-t` for the separator and not `'-'.repeat(32)` Because the separator is **part of the layout**, not part of the text. Drawing it as a CSS border keeps it sharp at any export resolution and survives a future change of mono font without checking whether that font ships a `─` glyph. WinAnsi encoding — what `pdf-lib`'s built-in fonts use — doesn't include box-drawing characters; tracking which custom fonts do gets tedious fast. ### Why `$` and not `₹` The fixture uses `$` as the currency symbol even though it's a Bengaluru bistro. Reason: the standard PDF built-in mono font (Courier) only encodes the WinAnsi range, and the rupee sign isn't in there. If you register a real mono TTF with rupee glyphs (and call `registerFont` before export), you can swap freely: ```tsx await registerFont({ family: "Mono", url: "/fonts/IBMPlexMono-Regular.woff2", weight: 400, }); ``` ### Why the totals are integers POS systems usually quote whole rupees / cents / paise — they round at line-item time, not at the display step. The fixture follows that convention so `Math.round` shows up explicitly rather than getting hidden behind a `toFixed(2)`. Swap to fractional cents trivially with a `Money` helper. ## Customizing it The smallest "swap your branding in" path: 1. Replace the `Header` block with your own business name + address. 2. Edit `RECEIPT_80MM` if you're on 58mm thermal paper instead. 3. Provide your own `order` shape — `{ orderNo, table, server, guests, items: [{ name, qty, unit, modifiers: [{ name, price }] }] }`. 4. Register a mono font with the glyphs your locale needs (currency symbols, accented characters). Total work: ~20 minutes to swap a real menu in. ## Related - [Best practices](/docs/best-practices) — width-matching, font registration, separators - [Fonts](/docs/fonts) — registering a custom mono font with currency glyphs - [Sizing and alignment](/docs/sizing-and-alignment) — how the custom paper size flows to `pageContentWidth` --- ## Statement of account Source: https://react-print-pdf-inky.vercel.app/docs/examples/statement > A multi-page bank statement with 64 transactions, repeating table header, and a closing-balance card glued to the end. ## What you're looking at A monthly statement \u2014 letterhead, account holder card, KPI summary, 64 transactions across three pages, and a closing-balance card at the end. This fixture exists in the workshop specifically to exercise the trickiest part of pagination: a **table that flows across pages followed by content that must sit flush against the last row**. Pagination here demonstrates: - **64 rows, ~3 A4 pages.** The paginator splits on row boundaries. - **`` that repeats across continuation pages.** Every page shows Date \u00b7 Reference \u00b7 Description \u00b7 Category \u00b7 Amount \u00b7 Balance. - **No phantom gap after the table.** The closing-balance card sits directly below the last transaction row, even when the table ends mid-page. (This was a real bug we fixed in v0.2 \u2014 the [QA target 2.1 comment in the fixture](https://github.com/hempun10/react-print-pdf/blob/main/examples/demo/src/StatementFixture.tsx) flags it.) - **Negative amounts in red, positive in default.** Plain Tailwind conditional classes. The walker reads computed colors per cell. Other features in play: - **Letterhead with `border-b-2`.** Two-pixel borders survive the walker without rounding artifacts. - **Left-accent KPI card.** The closing-balance card has a 4px green left border (`border-l-4 border-l-emerald-600`). Per-side colored borders are vector. - **Tabular nums for currency columns.** `tabular-nums` keeps decimals aligned regardless of digit count. ## The exporter call Statements are operationally similar to invoices. Headers and footers stay simple \u2014 a one-line band with the account number and page count: ```tsx const exporter = usePDFExport({ fileName: `statement-${period}.pdf`, paperSize: "A4", margins: { top: 48, right: 40, bottom: 40, left: 40 }, header: ({ pageNumber, totalPages }) => (
Cobalt Bank \u00b7 Account 04-3919-\u2026 Page {pageNumber} of {totalPages}
), headerHeight: 28, }); ``` ## The component [Full source on GitHub](https://github.com/hempun10/react-print-pdf/blob/main/examples/demo/src/StatementFixture.tsx). The three structural sections: ```tsx export function Statement({ customer, rows, opening, closing }) { const totalCredits = rows.filter(t => t.amount > 0).reduce((s, t) => s + t.amount, 0); const totalDebits = rows.filter(t => t.amount < 0).reduce((s, t) => s + t.amount, 0); return (
{/* 1. Letterhead */}
\u2026
{/* 2. Customer + summary band */}
{/* 3. The transactions table \u2014 the one that paginates */}

Transactions

{rows.map(t => )}
DateReferenceDescription CategoryAmountBalance
{/* 4. Closing balance carried forward \u2014 sits flush below the table */}
\u2026
); } ``` ## Why the choices look the way they do ### `` instead of CSS only Same reason as the invoice example. The data-attribute signal is unambiguous; we don't depend on print CSS being honored consistently across whatever bundling pipeline you happen to use. ### One closing-balance card before the table AND one after The card at the top is the **headline number**, the card at the bottom is the **legal carry-forward**. Banks have done it this way for decades because the reader sees the answer immediately at the top, and the legally-binding figure (with the period statement and "report discrepancies within 30 days" notice) appears at the close. The library handles both because they're just sections in document order. ### `align-top` on `` Multi-line descriptions in some transactions ("PURCHASE AUTH XXXXXXXX VISA \u2026") wrap to two lines. Without `align-top`, the date and reference would float to vertical center, which looks off. Try removing it once and you'll see what we mean. ### Why categories are styled as `text-gray-500`, not branded chips Chips look great on screen and ridiculous on a statement someone is going to print and file. The fixture deliberately keeps "PDF business document" aesthetics rather than "SaaS dashboard" aesthetics. Branding belongs in the letterhead. ## Variations - **Currency.** Replace `money()` and `moneySigned()` with `Intl.NumberFormat` for locale-aware separators and currency symbols. - **Date format.** `fmtDateLong` is ISO-ish; for US conventions swap to `Intl.DateTimeFormat("en-US", { dateStyle: "long" })`. - **Per-row tax columns.** Add columns to the table; the repeating header automatically picks them up. - **Multiple accounts in one PDF.** Wrap each `` in a section with `break-before: page` and pass them all to a single `exportToPDF` call. ## Related - [Pagination](/docs/pagination) \u2014 `` and continuation behavior - [Best practices](/docs/best-practices) \u2014 tabular nums, width-matching - [Production checklist](/docs/production-checklist) \u2014 pre-launch sweep for financial documents --- ## exportToPDF Source: https://react-print-pdf-inky.vercel.app/docs/export-to-pdf > The main export function. Pass an HTMLElement and receive a PDFExportResult handle. ```ts import { exportToPDF } from "react-print-pdf"; const result = await exportToPDF(element, options); ``` ## Signature ```ts function exportToPDF( element: HTMLElement, options?: ExportOptions, ): Promise; ``` `element` must be a mounted DOM node. In React, pass `ref.current`. ## Options ### `format` Page size. Presets are in millimetres internally. A tuple is `[widthMm, heightMm]`. ```ts format: "A4" format: "Letter" format: [210, 297] ``` Default: `"A4"`. ### `orientation` ```ts orientation: "portrait" | "landscape" ``` Default: `"portrait"`. ### `margin` Margins accept CSS-like units. Numbers are treated as CSS pixels. ```ts margin: "20mm" margin: "1in" margin: 24 // 24 CSS px margin: { top: "18mm", right: "16mm", bottom: "22mm", left: "16mm" } ``` Supported units: `mm`, `cm`, `in`, `px`, `pt`. Unitless strings are treated as pixels. Default: `0`. Set explicitly when you want printable margins. Common choice for documents: `"20mm"`. Headers and footers render inside the top / bottom margin band, so passing `margin: 0` together with `header` / `footer` will leave the bands no room to draw — set a margin (or accept that the bands won't render). ### `align` ```ts align: "left" | "center" | "right" ``` Horizontal alignment of body content within the page's content area (the area between left and right margins). - `"center"` (default) — if your DOM is narrower than the content area, the document is centered horizontally. Wider DOM is left-anchored at the left margin so nothing is clipped. - `"left"` — always anchor at the left margin (legacy behavior). - `"right"` — anchor at the right margin. This affects body primitives **and** repeat bands (e.g. ``), so column headers stay aligned with the rows underneath them across pages. ### `pageBreak` ```ts pageBreak: "auto" | "manual" | "single" ``` | Mode | Behavior | | --- | --- | | `auto` | Break at overflow and at `` markers | | `manual` | Break only at `` markers | | `single` | One page sized to the content bounds | Default: `"auto"`. ### `header` and `footer` Render functions called with page context: ```tsx header: ({ pageNumber, totalPages }) => (
Acme Corp Page {pageNumber} of {totalPages}
) ``` ```ts type PageContext = { pageNumber: number; totalPages: number; }; ``` Header/footer bands are rendered once for measurement, then re-rendered per page after total pages are known. ### `filename` Default filename used by `result.save()`. ```ts filename: "invoice.pdf" ``` ### `title` and `author` PDF metadata fields: ```ts title: "Invoice #1042" author: "Acme Corp" ``` ### `onProgress` ```ts onProgress?: (stage: ExportStage, pct: number) => void; ``` Stages: ```ts type ExportStage = "prepare" | "walk" | "paginate" | "emit" | "deliver"; ``` `pct` is `0` or `1` for each current stage. The library does not yet stream sub-stage progress. ### `onPageRendered` ```ts onPageRendered?: (pageNumber: number, totalPages: number) => void; ``` Fires once per page in page order, after the page is fully composed but before the document is serialized. Useful for streaming progress at per-page granularity, sending each page off as a thumbnail, or driving an external progress UI. Errors thrown from the hook propagate to the caller of `exportToPDF` and abort the export. ```ts await exportToPDF(printRef.current, { onPageRendered: (pageNumber, total) => { console.log(`rendered ${pageNumber}/${total}`); }, }); ``` ### `debug` ```ts debug?: boolean; ``` When `true`, the emitter draws a thin colored outline around every primitive's bounding box plus a small label in the top-left corner indicating the primitive kind. Useful when chasing a misaligned element or verifying which regions rasterized vs. drew vector. Off by default; ship `debug: false` to your users. Color key: blue `rect`, magenta `border`, red `text`, green `image`, olive `raster`, orange `link`, teal `softRect` / `softRectSlice`. ## Result ```ts type PDFExportResult = { bytes: () => Uint8Array; blob: () => Blob; save: (filename?: string) => void; preview: () => Window | null; }; ``` Example: ```ts const result = await exportToPDF(printRef.current, { format: "A4", margin: "20mm", filename: "invoice.pdf", }); result.save(); await uploadPDF(result.blob()); ``` --- ## Fonts Source: https://react-print-pdf-inky.vercel.app/docs/fonts > Register TTF/OTF fonts so browser layout and PDF output use the same face. PDFs can only embed font bytes the library can access. If your document uses a custom family, register each TTF/OTF face before calling `exportToPDF`. ## Register a face ```ts import { registerFont } from "react-print-pdf"; await registerFont({ family: "Inter", src: "/fonts/Inter-Regular.ttf", weight: 400, style: "normal", }); ``` The `family` value must match the CSS family used by your printable DOM. ```tsx
...
``` ## Type ```ts type FontDescriptor = { family: string; src: string; // URL to .ttf or .otf weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: "normal" | "italic"; }; ``` Defaults: ```ts weight: 400 style: "normal" ``` ## Multiple weights Register every weight/style pair your document uses. ```ts await Promise.all([ registerFont({ family: "Inter", src: "/fonts/Inter-Regular.ttf", weight: 400 }), registerFont({ family: "Inter", src: "/fonts/Inter-Italic.ttf", weight: 400, style: "italic" }), registerFont({ family: "Inter", src: "/fonts/Inter-Bold.ttf", weight: 700 }), registerFont({ family: "Inter", src: "/fonts/Inter-BoldItalic.ttf", weight: 700, style: "italic" }), ]); ``` ## What registration does `registerFont` performs two jobs: 1. Fetches the font bytes for PDF embedding. 2. Loads the same bytes into the browser via `FontFace`, so DOM layout uses the same face before the walker measures text. The exporter also waits for `document.fonts.ready` before walking. ## Supported formats | Format | Status | | --- | --- | | TTF | Supported | | OTF | Supported | | WOFF / WOFF2 | Not supported by `pdf-lib` font embedding | If your web app currently serves WOFF2 only, add a TTF or OTF asset for PDF export. ## Fallback behavior If a DOM text run uses an unregistered font family, the emitter falls back to a standard PDF font. The export continues, but metrics may drift and the PDF will not match the screen exactly. For production documents, register the actual family and the weights used in the printable subtree. --- ## How it works Source: https://react-print-pdf-inky.vercel.app/docs/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](/docs/api-reference) and [export-to-pdf](/docs/export-to-pdf) pages cover that. ## The five-stage pipeline [Diagram: the 5-stage export pipeline — React render → live DOM → walk styles → paginate → emit PDF bytes] 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 `` or `` wrapper to learn. Use Tailwind, use CSS modules, use styled-components, use design tokens. Whatever you already use for the screen. ```tsx function Invoice({ data }: { data: InvoiceData }) { return (
); } ``` 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 `` 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 `` 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 `
` 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 [Diagram: paper minus margins minus optional header / footer reserves equals content area] 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. `` 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 ``, 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. [Diagram: a 10-row table paginated across two A4 pages with a repeating column header and a legend of the three break hints — break-before, break-after, break-inside: avoid] 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](/docs/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 `…` 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](/docs/quick-start) — the 3-minute build - [Examples](/docs/examples/receipt) — real-world business documents with preview + code - [Pagination](/docs/pagination) — break hints, repeat bands, edge cases - [Best practices](/docs/best-practices) — what to do and what to avoid - [Limitations](/docs/limitations) — the short, honest list --- ## Images Source: https://react-print-pdf-inky.vercel.app/docs/images > PNG and JPEG img elements are embedded as reusable PDF image XObjects. `` elements are treated as image primitives. PNG and JPEG sources are embedded in the PDF instead of being captured as part of a larger screenshot. ## Supported sources | Source | Status | Notes | | --- | --- | --- | | `data:image/png;base64,...` | Supported | decoded without network | | `data:image/jpeg;base64,...` | Supported | decoded without network | | `/logo.png` | Supported | fetched from same origin | | `https://.../photo.jpg` | Supported | CORS must allow browser fetch | | `blob:` URL | Supported | fetched by the browser | | GIF / WebP / AVIF | Not supported | dropped with a warning | | SVG image file | Not supported as image | use raster fallback for SVG regions | ## Format detection The emitter sniffs magic bytes before embedding: - PNG: `89 50 4E 47` - JPEG: `FF D8 FF` The URL extension is not trusted. If a CDN returns HTML, a 404 page, or WebP behind a `.png` path, the image is skipped with a warning. ## Deduplication The same image source is embedded once and reused. ```tsx ``` Those three DOM nodes produce one PDF XObject. ## Sizing The image is drawn into the DOM box reported by `getBoundingClientRect()`. ```tsx ``` The PDF uses the CSS size, not the intrinsic image size. ## CORS Remote images must be readable by browser `fetch()`. If an image disappears in the PDF: 1. Open DevTools network panel. 2. Confirm the image request succeeds. 3. Confirm the response has an `Access-Control-Allow-Origin` policy that allows your app origin. 4. If not, host the image on your app origin or convert it to a data URL before rendering. ## Known gap `object-fit: fill | contain | cover` is honored. `fill` (the CSS default) stretches the image to the box; `contain` letterboxes it inside the box, keeping aspect ratio; `cover` fills the box and clips the overflow via a PDF clip path. `none` and `scale-down` are not modeled and collapse to `fill`. --- ## Installation Source: https://react-print-pdf-inky.vercel.app/docs/installation > Install react-print-pdf from npm. `react-print-pdf` is published to npm as a stable release. ## Install ```bash npm install react-print-pdf ``` The current stable version is `0.2.0`. Earlier prereleases (`0.1.0-alpha.0`, `0.2.0-beta.*`) are still available under the `alpha` and `beta` dist-tags for projects that pinned to them, but new code should target `latest`. ## Peer dependencies The package targets browser apps using React 18 or React 19: ```json { "peerDependencies": { "react": ">=18", "react-dom": ">=18" } } ``` ## Try the repository locally If you want to hack on the library itself or run the demo: ```bash git clone https://github.com/hempun10/react-print-pdf.git cd react-print-pdf npm install npm run build ``` Run the demo app: ```bash cd examples/demo npm install npm run dev ``` Open the local URL, choose a fixture, and click **Export PDF**. ## Try it without cloning A minimal starter ships under [`examples/starter`](https://github.com/hempun10/react-print-pdf/tree/main/examples/starter). Open it in StackBlitz directly off the GitHub path: ``` https://stackblitz.com/github/hempun10/react-print-pdf/tree/main/examples/starter ``` The starter is a standard Vite + React + `react-print-pdf` app: one Invoice component and one Export PDF button. Replace `Invoice.tsx` with your own document to evaluate the library against real content. ## Browser requirements The exporter runs in the browser because it depends on real layout information: - `getComputedStyle()` - `getBoundingClientRect()` - `Range.getClientRects()` - `document.fonts.ready` - `FontFace` - `CanvasRenderingContext2D` - `fetch()` for image/font bytes - `URL.createObjectURL()` for save/preview helpers Modern Chromium, Firefox, and WebKit are covered by the test suite. ## Runtime dependencies The library uses: | Dependency | Role | | --- | --- | | `pdf-lib` | PDF document creation and drawing | | `@pdf-lib/fontkit` | TTF/OTF parsing and font embedding | | `html-to-image` | Raster fallback for browser-only effects | The library entry is around 10 kB gzipped; the full bundle (with `pdf-lib` + `fontkit` + `html-to-image`) is around 440 kB gzipped. ## TypeScript Types ship with the package. The public API is intentionally narrow: ```ts import { exportToPDF, registerFont, PageBreak, PRINT_REPEAT_ATTR, PdfExportError, isPdfExportError, // React DX layer usePDFExport, Printable, ExportButton, PRINTABLE_ATTR, type ExportOptions, type PDFExportResult, type PdfExportErrorCode, type UsePDFExportOptions, type UsePDFExportReturn, type PrintableProps, type ExportButtonProps, } from "react-print-pdf"; ``` --- ## Introduction Source: https://react-print-pdf-inky.vercel.app/docs/introduction > Export the React you've already built to a real vector PDF — no headless browser, no second component language. You've already built the screen. Your invoice, statement, receipt, or report renders in the browser, looks the way the designer drew it, prints from Chrome, and your QA team has stopped filing tickets about it. Then product says: **"can users download a PDF?"** That single feature usually triggers one of three sad architectures: spin up a Chromium fleet on the server and pray it doesn't OOM; rebuild the whole document in a parallel renderer with its own CSS subset; or screenshot the screen to a canvas and ship a fuzzy image where the text used to be selectable. `react-print-pdf` is the fourth option. It takes the DOM the browser already painted and emits a real vector PDF in the same tab — selectable text, embedded fonts, hyperlinks, the works. No server, no second renderer, no canvas blur. [Diagram: the 5-stage export pipeline — React render → live DOM → walk styles → paginate → emit PDF bytes] ## What this is A small library that: - runs in the browser (no Node, no server fleet) - reads the DOM your existing React component already laid out - emits PDF primitives via [`pdf-lib`](https://pdf-lib.js.org/) — selectable text, embedded TTF/OTF fonts, real PNG/JPEG images, real hyperlinks - handles pagination, repeating headers and footers, manual page breaks - falls back to raster for the handful of effects that don't translate cleanly (gradients, shadows, filters, SVG roots) - ships React DX: a `usePDFExport` hook, a `` wrapper, an `` It does **not**: - ask you to rewrite anything - need its own component language - require a server - give up on selectable text because of a single drop shadow ## Why bother Most teams arrive at PDF after they already own a document UI: React components, Tailwind utilities, design tokens, branded tables, typography rules. Throwing that work away to "do PDF properly" is the most expensive option, not the cleanest. Here's how the common paths trade off: | Tooling path | What you get | What you pay | | --- | --- | --- | | Headless Chromium | Accurate screenshots or print output | Server fleet, cold starts, ~200MB binaries, operational overhead | | Parallel renderers (e.g. `@react-pdf/renderer`) | Browser-independent PDF generation | A second layout/component system and a smaller CSS subset | | Canvas screenshot export | Visual fidelity | Blurry text, no selection, large files, inaccessible output | | **react-print-pdf** | Browser layout + vector PDF primitives | Browser-only for v1, plus a short list of [honest limitations](/docs/limitations) | ## How it actually works Five stages. You only write the first one — the browser does stage two for free, we do stages three through five. 1. **You render.** A regular React component. Tailwind, CSS modules, whatever you already use. 2. **The browser paints.** Computed styles, font metrics, line wrapping, table layout — already solved, already in the DOM. 3. **We walk.** We read every box, every text rectangle, every image, every border, and tag the regions that need raster fallback. 4. **We paginate.** Split the captured tree into pages, repeat your header and footer, honor `break-before` / `break-after` / `break-inside: avoid`. 5. **We emit.** `pdf-lib` does the byte serialization, fonts get subset, images get embedded, links become clickable annotations. Want the longer version with examples? Read [How it works](/docs/how-it-works). Want to ship something today? [Quick start](/docs/quick-start) takes three minutes. ## What's vector today Everything the browser draws with shapes and text, we draw with shapes and text: - text runs from `Range.getClientRects()` — exactly where the browser put them - solid backgrounds, including ones that continue across page breaks - borders, including per-side colors and rounded corners - hyperlinks: every `` becomes a clickable PDF Link annotation - PNG and JPEG images as PDF image XObjects (real bytes, not screenshots) - registered TTF/OTF fonts, subset into the final PDF - pagination, headers, footers, explicit breaks, repeat bands - true alpha — semi-transparent fills compose correctly against everything underneath ## What falls back to raster Some browser effects are expensive or unreliable to recreate as PDF vectors. The library automatically captures those regions as PNGs at high DPI: - gradients - shadows - CSS transforms - filters - SVG roots - anything you tag manually with `data-pdf-raster="true"` The goal isn't ideological purity — it's a PDF that stays mostly vector while preserving the parts the browser can draw better than a hand-rolled PDF emitter could. ## Status Public, MIT-licensed, semver-stable from `0.2.0`. - **Current channel:** `0.2.0` on npm under the `latest` dist-tag - **Scope:** browser-only, React-only for v1 - **API shape:** pass an `HTMLElement` to `exportToPDF`, or use the React DX layer (`usePDFExport`, ``, ``) - **Honest about gaps:** the [Limitations](/docs/limitations) page is short, current, and not buried ## Where to next | If you want to | Go here | | --- | --- | | See it work in three minutes | [Quick start](/docs/quick-start) | | Understand the export pipeline | [How it works](/docs/how-it-works) | | Copy a real-world example | [Examples](/docs/examples/receipt) | | Know what to avoid before production | [Best practices](/docs/best-practices) → [Limitations](/docs/limitations) | | Wire your AI coding tool | [AI guide](/docs/ai-guide) | --- ## Limitations Source: https://react-print-pdf-inky.vercel.app/docs/limitations > The honest 0.2 beta boundaries. `react-print-pdf` is at `0.2.0`. It is useful today for controlled document exports, but it is not a complete browser print engine. ## Runtime - Browser-only for v1. - React-only public integration for now. - `exportToPDF` takes an `HTMLElement`, not JSX. - No Node/SSR export path yet. ## Layout - The printable root should fit the PDF content width. - A4 portrait with `20mm` margins is roughly `640px` wide in DOM space. - Complex CSS Grid layouts need visual verification. - Auto-fit / scale-to-page is not implemented yet. ## Drawing - `border-radius` is honored for fills and for borders, including mixed per-side widths via an even-odd donut fill. - Mixed per-side border colors are simplified. - Text decoration (`underline`, `line-through`, `overline`) is drawn; text shadow is not. - Letter spacing and word spacing can drift because text is emitted as PDF text runs. - SVG is rasterized, not converted to PDF vector paths. - `` to `http(s)`, `mailto:`, and `tel:` URIs becomes a clickable PDF Link annotation, with one annotation per visual line. Internal anchors (`href="#id"`) and unsafe schemes (`javascript:`, `data:`, etc.) are skipped — the text still renders. ## Pagination - Widows and orphans are ignored. - Repeat-band space is reserved conservatively. - `break-inside: avoid` can leave extra space at the page bottom. - A `break-inside: avoid` block taller than a page will split. - Tall cards (`background-color` + uniform border) split cleanly across pages with rounded corners only on start/end. Mixed per-side border colors and gradient backgrounds disqualify the split path — they fall back to the legacy atomic rectangle and may leave blank space on continuation pages. ## Images - Direct image embedding supports PNG and JPEG only. - GIF, WebP, and AVIF are dropped. - `object-fit: fill | contain | cover` is honored on ``. `none` and `scale-down` collapse to the default `fill`. - Remote images need CORS access. ## Raster fallback - Rasterized text is no longer selectable. - Large raster regions increase PDF size. - Cross-origin assets inside raster regions can fail to inline. ## Security and privacy The library runs in the browser and does not upload your DOM or PDF bytes anywhere. Your app is still responsible for where it sends the resulting `Blob` or `Uint8Array`. ## Roadmap candidates Likely follow-on work for `0.2.0` final and `0.3`: 1. Internal anchor support (`href="#section"`) alongside PDF outline tree 2. Debug overlay (`?debug` flag) drawing primitive boxes on top of the PDF preview 3. SVG-to-vector experiments (today SVGs go through the raster fallback) --- ## Pagination Source: https://react-print-pdf-inky.vercel.app/docs/pagination > Page breaks, headers, footers, break-inside avoidance, and repeating table headers. Pagination is part of the export pipeline. The library slices DOM-space primitives into page-space output while preserving manual breaks, page bands, and repeat regions. ## Page break modes ```ts pageBreak: "auto" | "manual" | "single" ``` | Mode | Use it for | | --- | --- | | `auto` | Reports, invoices, statements, long tables | | `manual` | Documents where you control every page boundary | | `single` | Receipts or one long custom page | ## Manual breaks Use the React marker: ```tsx import { PageBreak } from "react-print-pdf"; ``` Or CSS page-break properties on real elements: ```tsx
Appendix
Summary
``` Modern `break-before` / `break-after` and legacy `page-break-before` / `page-break-after` are both recognized. Accepted values: `page`, `always`, `left`, `right`. The walker emits a synthetic page break primitive at the element's top edge for `break-before`, and at its bottom edge for `break-after`. `auto`, `avoid`, and unset are no-ops. ## Keep a block together Use `break-inside: avoid` for cards, table-like rows, signatures, or approval blocks that should not split across pages. ```tsx

Approval

Keep this block intact.

``` If the block is taller than the available page content height, the library warns and splits it rather than clipping content. ## Headers and footers ```tsx await exportToPDF(printRef.current, { header: ({ pageNumber, totalPages }) => (
Q4 Revenue Report {pageNumber} / {totalPages}
), footer: () =>
Confidential
, }); ``` Header and footer components are measured at the printable content width. Their primitives are placed in the top and bottom margin bands. ## Repeating table headers Add `data-print-repeat` to the element that should repeat on continuation pages. ```tsx {rows}
SKU Description Amount
``` You can import the attribute constant if you prefer not to hardcode the string: ```ts import { PRINT_REPEAT_ATTR } from "react-print-pdf"; PRINT_REPEAT_ATTR === "data-print-repeat"; ``` ## Current trade-offs The paginator is intentionally conservative: - `break-inside: avoid` can leave unused space near the page bottom - widows and orphans are not implemented - tall cards with a `background-color` and a uniform border continue cleanly across page breaks (rounded corners on start/end pages only, square edges in the middle). Mixed per-side border colors and gradient backgrounds disqualify the soft-split path — they fall back to a single atomic rectangle and may leave blank space on continuation pages. See [RFC 0001](https://github.com/hempun10/react-print-pdf/blob/main/rfc/0001-soft-card-backgrounds.md) for the design. Those are layout quality improvements, not blockers for the core pipeline. ## Repeat-band scope A repeat band only takes up space on **continuation pages of its parent**. A `` inside a `` reserves space at the top of every page that the table spans — not on pages before the table starts, and not on pages after the table ends. Content that follows the table sits flush against the last row, not below a phantom header gap. --- ## Print preview Source: https://react-print-pdf-inky.vercel.app/docs/print-preview > Show users exactly how their document will paginate — paper size, margins, header/footer bands, page-break boundaries — without going through the export pipeline. You don't always want to wait for a PDF to download just to see whether a page break landed in the right place. `` (and the lower-level `usePrintPreview` hook) shows that **inline, while the user types**, using the same paginator that drives `exportToPDF`. What you see in the preview is what the file will contain — same page sizes, same margins, same header / footer, same break points. No canvas, no `pdf.js`, no round-trip through the PDF emitter. ## When to reach for it - **Document editors.** Users adjust line items, the preview re-paginates in 150ms. - **Onboarding screens.** Show the user "this is what your invoice will look like" before they pay for the export. - **QA / form review.** Surface a "review before download" step so support tickets stop asking "where did page 2 come from?" - **Live previews in marketing pages.** Drop `` next to a form and let visitors watch the document change as they fill it out. If you only need the file at the end (no in-app preview), keep using `exportToPDF` / `usePDFExport` directly — there's no reason to add this layer. ## Quick start ```tsx import { PrintPreview } from "react-print-pdf"; export function InvoicePreview({ data }: { data: InvoiceData }) { return ( ); } ``` That's it. The component renders a vertical stack of scaled page cards, each one a real DOM clone of your children clipped at the computed page boundary. Edit `data`, watch the cards re-paginate. ### With header and footer `` accepts the same `header` / `footer` renderers as `exportToPDF`. They run per page card and receive the live `PageContext`: ```tsx (
Acme · Invoice {ctx.pageNumber} / {ctx.totalPages}
)} footer={(ctx) => (
Generated with react-print-pdf · page {ctx.pageNumber}
)} >
``` Pass `headerHeight` / `footerHeight` so the paginator reserves the right amount of room. They're approximations — the cards don't measure your bands automatically (the PDF emitter does, but doing it in preview would add a second layout pass per render). ### Pairing preview with export The natural pattern is to show the preview on screen and put an "Export" button next to it that runs the same options through `exportToPDF`. Share the options object so the two surfaces never drift: ```tsx import { PrintPreview, usePDFExport, ExportButton } from "react-print-pdf"; const options = { format: "A4" as const, margin: "20mm", header: PageHeader, footer: PageFooter, }; export function InvoiceWithPreview({ data }: { data: InvoiceData }) { const { ref, exportPDF, isExporting } = usePDFExport({ ...options, filename: `${data.invoiceId}.pdf`, }); return (
Download PDF
); } ``` Note that `` still needs to receive the export `ref` (or be inside something that does) so `exportToPDF` has a live DOM element to walk. `` mounts a second invisible copy internally — they don't compete. ## API ### `` props | Prop | Type | Default | Notes | | - | - | - | - | | `format` | `PageFormat` | `"A4"` | Same as `exportToPDF`. Accepts named formats or `[width_mm, height_mm]`. | | `orientation` | `"portrait" \| "landscape"` | `"portrait"` | | | `margin` | `Margin` | `0` | String / number / per-side object. Same shape as the exporter. | | `pageBreak` | `"auto" \| "manual" \| "single"` | `"auto"` | | | `align` | `"left" \| "center" \| "right"` | `"center"` | Reported for parity; the v1 preview is left-anchored visually. | | `header` | `(ctx) => ReactElement` | — | Renders per page in the top margin band. | | `footer` | `(ctx) => ReactElement` | — | Renders per page in the bottom margin band. | | `headerHeight` | `number` | `0` | CSS px reserved at the top. Set this when you pass `header`. | | `footerHeight` | `number` | `0` | CSS px reserved at the bottom. | | `scale` | `number` | `0.85` | Visual zoom. Doesn't affect pagination math. | | `gap` | `number` | `24` | CSS px between page cards (scaled). | | `pageBackground` | `CSSProperties["background"]` | `"white"` | Background of each page card. | | `showPageNumbers` | `boolean` | `true` | Tiny `1 / N` badge in the bottom right of each card. | | `className`, `style` | — | — | Applied to the outer scroll container. | | `children` | `ReactNode` | — | Must be pure — see [Caveats](#caveats). | ### `usePrintPreview()` hook `` is a thin wrapper over `usePrintPreview()`. Use the hook directly when you want full control over the page UI: ```tsx import { usePrintPreview } from "react-print-pdf"; function CustomPreview({ children }: { children: ReactNode }) { const { ref, pages, status, contentWidth, pageWidth, pageHeight } = usePrintPreview({ format: "A4", margin: "20mm" }); return ( <> {/* Off-screen mounting container, your responsibility. */}
{children}
{/* Your own page UI, computed from pages[]. */} {pages.map((page) => ( ))} {status === "measuring" && } ); } ``` The hook returns: | Field | Type | Notes | | - | - | - | | `ref` | `MutableRefObject` | Attach to your off-screen mount. | | `pages` | `PreviewPage[]` | One entry per computed page; carries `pageNumber`, `totalPages`, `yOffset`, `margin`, `contentHeight`, `context`, `hasRepeatBand`. | | `totalPages` | `number` | Convenience accessor. | | `contentWidth` | `number` | CSS px width of the page content area (page width minus L+R margins). | | `pageWidth`, `pageHeight` | `number` | CSS px of the paper. | | `status` | `"idle" \| "measuring" \| "ready" \| "error"` | Render gating. | | `error` | `Error \| null` | Last thrown error, cleared on next successful measure. | | `recompute` | `() => void` | Manual debounced re-measure. | ### `PreviewPage` shape Each entry in `pages[]`: ```ts interface PreviewPage { pageNumber: number; totalPages: number; width: number; // CSS px paper width height: number; // CSS px paper height margin: { top: number; right: number; bottom: number; left: number }; yOffset: number; // DOM Y of this slice's top contentHeight: number; // available CSS px after repeat-band reserve hasRepeatBand: boolean; // true on continuation pages of a repeating thead repeatBandReservePx: number; context: { pageNumber: number; totalPages: number }; } ``` ## How it works The preview runs the **PREPARE → WALK → PAGINATE** stages of the exporter, then stops. It does not emit a PDF. 1. Your children get mounted **once** in an off-screen container clipped to zero pixels with `clip-path`. Layout still runs, so widths, line wraps, and computed heights are real. 2. The walker reads computed styles + bounding rects and emits the same primitive list `exportToPDF` would. 3. The paginator slices that list into `Page` objects with `yOffset` and `contentHeight`. Same code path, same behavior — including `break-inside: avoid`, `data-print-repeat`, `pageBreak="manual"` markers, and the soft-card heuristic from RFC 0001. 4. Each visual page card overlays: - A header band (your JSX in the top margin) - A clip window (`overflow: hidden`) showing the mounted DOM clone shifted up by `yOffset` - A footer band 5. Resizes of the mounted container fire a debounced 150ms recompute. The latest measure always wins via a generation counter. Because step 4 paints **the same DOM** the walker measured, what you see is what the PDF will draw — there's no separate renderer that can drift. ## Caveats The v1 preview optimizes for "show me the page boundaries with minimum overhead." It has known limits: - **Read-only.** Each page card injects a snapshot of the measured DOM via `dangerouslySetInnerHTML`. Inputs, click handlers, and React effects inside the clones don't fire. Forms still work if you put them outside ``. - **Repeat bands are placeholders.** A continuation page that would re-render `
` shows a diagonal-stripe placeholder of the right height. The paginator math is correct; only the visual repaint of the band is deferred. Full rendering is planned for the next minor. - **Soft cards continuation borders are clipped, not redrawn.** A card that crosses a page boundary in the PDF gets clean top/bottom borders drawn per slice (RFC 0001). In the preview, the same card's borders are just clipped by the page window — visually similar, not identical. - **No raster fallback.** Charts and effects render as live DOM in the preview, not as the PNG they'd become in the PDF. Usually that's an upgrade (sharper), but if you're debugging rasterization specifically, export the PDF. - **Single mounted copy.** The preview can't show interactive variants per page. If you need a per-page editor, the `usePrintPreview` hook gives you the page metadata; build your own UI on top. ## Performance The preview is intentionally cheaper than a full export. For a typical multi-page invoice (50 line items): | Stage | Cost | | - | - | | Mount + first layout | one React commit | | Walk + paginate | ~5–15ms on a modern laptop | | Snapshot via `outerHTML` | ~1–3ms per measure | | Per-card DOM clip | cheap; the browser shares layout across siblings | Live edits trigger one re-measure (debounced 150ms). The full export still costs walk + paginate **plus** emit + serialize + raster — orders of magnitude more work — so showing the preview costs almost nothing on top of "the user types and the page re-renders." If your source is huge (200+ pages), pass `pageBreak="single"` while editing and switch to `"auto"` only when the user is reviewing. ## Related - [`exportToPDF`](/docs/export-to-pdf) — the underlying export call. Preview shows you what this would produce. - [`React API`](/docs/react-api) — `usePDFExport`, `Printable`, `ExportButton`. Compose these with `` for a full "edit / preview / download" UX. - [Pagination](/docs/pagination) — page breaks, headers, footers, repeat bands. The preview honors all of it. - [Sizing and alignment](/docs/sizing-and-alignment) — how `pageContentWidth` ties your DOM width to the page. The preview's mounting container is sized with the same helper. --- ## Production checklist Source: https://react-print-pdf-inky.vercel.app/docs/production-checklist > What to verify before using the beta in a customer-facing workflow. Use this checklist before a pilot or customer-facing export flow. ## Document layout - Printable root has an explicit width. - Width fits the target page content area. - A4 + `20mm` margins starts around `640px` DOM width. - Long rows/cards that should stay together use `break-inside: avoid`. - Manual chapter/report breaks use `` or `break-before: page`. ## Fonts - Every production font is available as TTF or OTF. - Every used weight/style is registered before export. - DOM CSS `font-family` matches the registered family name. - Fallback PDF output has been checked by intentionally disabling a font URL once. ## Images - Images are PNG or JPEG. - Remote images pass CORS. - Logo/images repeated across pages use the same `src` so deduplication works. - Any `object-fit` dependency has been visually checked or avoided. ## Raster fallback - Complex visual widgets are intentionally marked with `data-pdf-raster="true"`. - Large raster regions are avoided. - Raster text is acceptable for those regions. - Cross-origin fonts/images inside raster regions are self-hosted or CORS-enabled. ## Pagination - Headers and footers fit inside the configured margins. - Multi-page tables use `data-print-repeat` on `` or an equivalent header band. - Generated PDFs have been checked at 100%, 200%, and text-selection view. - Page count and page-number labels have been verified on the longest known document. ## Delivery - Use `result.save()` for direct download. - Use `result.blob()` for upload or preview in your own UI. - Use `result.bytes()` for tests and low-level integrations. - Do not keep object URLs forever; the built-in `save()` and `preview()` helpers revoke their temporary URLs. ## Browser support Run the same export fixture in: - Chromium - Firefox - WebKit/Safari The repo already uses Playwright across browser engines. Add your own customer document fixtures once the export becomes part of a critical workflow. --- ## Quick start Source: https://react-print-pdf-inky.vercel.app/docs/quick-start > Export a rendered React document by passing its DOM element to exportToPDF. `exportToPDF` works on an `HTMLElement`. Render your React document normally, keep a ref to the printable root, and pass `ref.current` to the exporter. ## Try it in your browser first The fastest way to see the library work end-to-end is the official starter on StackBlitz. It boots a Vite + React app with `react-print-pdf` already installed and a small invoice document wired to an **Export PDF** button. [Open the starter on StackBlitz](https://stackblitz.com/github/hempun10/react-print-pdf/tree/main/examples/starter) The full source lives in [`examples/starter/`](https://github.com/hempun10/react-print-pdf/tree/main/examples/starter) so you can also clone it locally. ## 1. Create a printable component Give the printable area an explicit width that fits your target page. For A4 portrait with `20mm` margins, `640px` is a safe starting point. ```tsx title="Invoice.tsx" export function Invoice() { return (

Invoice #1042

Acme Corp · Due May 31

Item Amount
Platform license $250.00
Total $250.00
); } ``` ## 2. Export from a button Two equivalent paths. Pick the one that fits your call site. ### Option A — hook + `` (recommended) The React DX layer owns the ref, loading state, and error state for you, and `` sizes the container to the page automatically. You can drop the explicit `w-[640px]` from your Invoice component when you wrap it in ``. ```tsx title="ExportButton.tsx" import { Printable, usePDFExport } from "react-print-pdf"; import { Invoice } from "./Invoice"; export function ExportButton() { const { ref, exportPDF, isExporting, error } = usePDFExport({ format: "A4", margin: "20mm", filename: "invoice.pdf", }); return ( <> {error &&

{error.message}

} ); } ``` Full reference for the hook and components on the [React API](/docs/react-api) page. ### Option B — the functional API Use `exportToPDF` directly when you want full control or you're outside React. ```tsx title="ExportButton.tsx" import { useRef } from "react"; import { exportToPDF } from "react-print-pdf"; import { Invoice } from "./Invoice"; export function ExportButton() { const printRef = useRef(null); async function exportInvoice() { if (!printRef.current) return; const result = await exportToPDF(printRef.current, { format: "A4", margin: "20mm", filename: "invoice.pdf", }); result.save(); } return ( <>
); } ``` ## 3. Register custom fonts If your document uses a non-standard font, register the TTF/OTF URL before exporting. ```tsx import { registerFont } from "react-print-pdf"; await registerFont({ family: "Inter", src: "/fonts/Inter-Regular.ttf", weight: 400, }); await registerFont({ family: "Inter", src: "/fonts/Inter-Bold.ttf", weight: 700, }); ``` The registered family must match the CSS `font-family` used by the DOM. The library waits for browser fonts before walking layout, then embeds the registered bytes in the PDF. ## 4. Use the result ```ts const result = await exportToPDF(printRef.current, options); result.save(); // download with options.filename result.save("copy.pdf"); // download with override result.preview(); // open object URL in a new tab const bytes = result.bytes(); const blob = result.blob(); ``` Use [exportToPDF](/docs/export-to-pdf) for the full options reference. --- ## Raster fallback Source: https://react-print-pdf-inky.vercel.app/docs/raster-fallback > Browser effects that are not cheap PDF vectors are captured as PNG regions. The exporter keeps simple document primitives vector, then falls back to raster for CSS effects that are expensive or risky to reproduce manually. ## Automatic triggers An element is rasterized when it uses one of these features: | Trigger | Example | | --- | --- | | `background-image` | gradients | | `box-shadow` | soft cards, elevation | | `filter` | blur, grayscale, drop-shadow | | `transform` | rotate, scale, skew | | SVG root | inline charts/icons that need preservation | The walker emits one raster primitive for the outermost matching region and skips its children, because the PNG already contains the subtree. ## Manual trigger Use `data-pdf-raster="true"` when you know a component is visually complex. ```tsx
``` This is useful for charts, maps, signatures, canvas-backed widgets, or any third-party component whose visual output matters more than selectable text. ## How it works The prepare phase scans the DOM before walking. Matching regions are captured with `html-to-image` at a pixel ratio of at least `2`. ```txt DOM region ↓ html-to-image PNG data URL ↓ cached by element RasterPrimitive ↓ embedded once by pdf-lib PDF image XObject ``` ## Trade-offs Raster fallback is intentionally selective: - large raster regions increase PDF size - text inside raster regions is not selectable - CORS-restricted images or fonts inside the region may fail to inline - very high device pixel ratios can create large PNGs Use raster fallback for visual effects, not as the default export strategy. ## When to force raster Good candidates: - brand-heavy cover sections - charts with gradients or SVG layers - transformed stamps or signatures - cards where the shadow is part of the visual contract Poor candidates: - long paragraphs - tables with hundreds of rows - legal terms that should remain searchable/selectable --- ## React API Source: https://react-print-pdf-inky.vercel.app/docs/react-api > usePDFExport, Printable, and ExportButton — sugar on top of the imperative exportToPDF function. `exportToPDF(element, options)` is the primitive: it takes an `HTMLElement`, runs the pipeline, and returns a result handle. Everything else on this page is sugar on top of that one function so the common case is one component or one hook call. ## When to use what | Tool | Use it when | | --- | --- | | `exportToPDF` | You're outside React, or you want full control over the lifecycle. | | `usePDFExport` | You're in a React component and want the ref + loading state + error state to come for free. | | `` | You want the printable container's width to match the PDF page automatically. | | `` | You want a working button in one component, no boilerplate. | You can mix and match. The hook + `` pair is the most common combination. ## `usePDFExport` ```tsx import { Printable, usePDFExport } from "react-print-pdf"; function InvoiceExport() { const { ref, exportPDF, isExporting, error, lastResult, reset } = usePDFExport({ format: "A4", margin: "20mm", filename: "invoice.pdf", title: "Invoice #1042", }); return ( <> {error && (
{error.message}
)} ); } ``` ### Returned values | Field | Type | Meaning | | --- | --- | --- | | `ref` | `MutableRefObject` | Attach to the printable container. | | `exportPDF` | `() => Promise` | Trigger an export. Resolves when the export finishes (success **or** failure). | | `isExporting` | `boolean` | True between the moment `exportPDF()` is called and the moment its work completes. | | `error` | `Error \| null` | Last error. Cleared when a fresh export starts or on `reset()`. | | `lastResult` | `PDFExportResult \| null` | Result from the most recent successful export. | | `reset` | `() => void` | Clear `error` and `lastResult` without triggering an export. | ### Hook-only options In addition to every field on [`ExportOptions`](/docs/export-to-pdf), the hook accepts: | Option | Default | Meaning | | --- | --- | --- | | `autoSave` | `true` | Calls `result.save()` automatically after a successful export. Set `false` for upload-only flows. | | `onSuccess` | — | Fires after a successful export, before the optional auto-save. Receives the `PDFExportResult`. | | `onError` | — | Fires when the export throws. Receives the `Error` (typed as `PdfExportError` when it originates from the library). | ### Generic ref typing The hook is generic in the element type so the ref attaches to whatever container you use: ```tsx const { ref } = usePDFExport(); // matches a or any element const { ref } = usePDFExport(); // strictly a
``` The default is `HTMLDivElement` to match the most common case. ### Stable identity `exportPDF` and `reset` are stable across renders, so it's safe to put them in `useEffect` / `useCallback` dependency arrays. Options are read fresh from a ref on every call, so changing `filename` or `margin` between renders takes effect immediately without forcing the hook to re-create its handlers. ## `` A `forwardRef`'d `
` that: 1. Sets `width` to `pageContentWidth({ format, margin, orientation })` so the source DOM matches the destination PDF. 2. Tags itself with `data-rpp-printable` so future tooling (debug overlay UI, optional ESLint plugin) can find printable regions. ```tsx ``` ### Props | Prop | Type | Default | Meaning | | --- | --- | --- | --- | | `format` | `PageFormat` | `"A4"` | Page format used to compute width. | | `orientation` | `Orientation` | `"portrait"` | Page orientation used to compute width. | | `margin` | `Margin` | `"20mm"` | Page margin used to compute width. | | `autoWidth` | `boolean` | `true` | Set false to opt out of automatic sizing (e.g. nesting inside another sized container). | Every other `
` prop (`className`, `style`, `role`, `data-*`, …) is forwarded. The component uses `forwardRef`, so the ref always lands on the underlying `
`. Pair it with `usePDFExport().ref`: ```tsx const { ref } = usePDFExport({ format: "A4", margin: "20mm" }); return ; ``` When you pass conflicting margins / formats to the hook and the component, the hook's options drive the export and the component's drive the on-screen size. Keep them in sync. ## `` A ` ); } ``` Now `align` is irrelevant — the DOM and the content area are the same width, so there's nothing to align. ## When you don't know the page format up front If you let users pick A4 vs Letter at runtime, recompute the width from the same options you pass to `exportToPDF`: ```tsx const opts = { format, orientation, margin }; const width = pageContentWidth(opts); return ( <>
...
); ``` ## Single source of truth (recommended pattern) The most common bug is sizing the container from one set of options and exporting with a different set. The library does not auto-shrink your DOM, so a width / margin mismatch silently clips the right edge of the page. Keep one options object and use it everywhere: ```tsx const pageOpts = { format: "A4", margin: "10mm" } as const;
``` If the source DOM is wider than the page content area at export time, `exportToPDF` logs a console warning explaining what it measured, what was expected, and how to fix it. There is no silent clipping with a debugger attached. ## Authoring receipts and single-page documents If paper size doesn't matter — you just want a PDF that's exactly the size of your DOM — use [`pageBreak: "single"`](/docs/pagination): ```tsx exportToPDF(ref.current, { pageBreak: "single", margin: 0 }); ``` The page is sized to your content's bounding box. No slack, no clipping, no worries about A4 vs Letter. ## Why is `align` "center" by default? Because the most common user mistake is hard-coding a container width that happens to be slightly narrower than the content area (e.g. `w-[640px]` on A4 with 10mm margins). With `align: "left"` that produces a lopsided PDF. With `align: "center"` the slack is split evenly so the document still looks balanced even when the user hasn't sized their root container precisely. If you size your container with `pageContentWidth(...)`, the alignment choice doesn't matter — there's no slack to distribute. ## Summary | Goal | Recipe | | --- | --- | | Exactly fill the page | Size root with `pageContentWidth({ format, margin })` | | Center a fixed-width DOM | Default behavior (no extra config) | | Left-anchor a fixed-width DOM | `align: "left"` | | Page sized to content | `pageBreak: "single"` | | Edge-to-edge content | `margin: 0` plus `pageContentWidth({ format })` for width | ---