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. <PrintPreview> (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.
exportToPDF, so the page breaks you see here are the page breaks you'll get in the file.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
<PrintPreview>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
import { PrintPreview } from "react-print-pdf";
export function InvoicePreview({ data }: { data: InvoiceData }) {
return (
<PrintPreview format="A4" margin="20mm">
<Invoice data={data} />
</PrintPreview>
);
}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
<PrintPreview> accepts the same header / footer renderers as exportToPDF. They run per page card and receive the live PageContext:
<PrintPreview
format="A4"
margin="20mm"
headerHeight={40}
footerHeight={28}
header={(ctx) => (
<div className="flex w-full items-center justify-between border-b py-2">
<span>Acme · Invoice</span>
<span>{ctx.pageNumber} / {ctx.totalPages}</span>
</div>
)}
footer={(ctx) => (
<div className="w-full pt-2 text-center text-[10px] text-gray-500">
Generated with react-print-pdf · page {ctx.pageNumber}
</div>
)}
>
<Invoice data={data} />
</PrintPreview>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:
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 (
<div className="grid gap-6 lg:grid-cols-[1fr_auto]">
<PrintPreview {...options}>
<Invoice ref={ref} data={data} />
</PrintPreview>
<ExportButton onClick={exportPDF} isExporting={isExporting}>
Download PDF
</ExportButton>
</div>
);
}Note that <Invoice> still needs to receive the export ref (or be inside something that does) so exportToPDF has a live DOM element to walk. <PrintPreview> mounts a second invisible copy internally — they don't compete.
API
<PrintPreview> 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. |
usePrintPreview() hook
<PrintPreview> is a thin wrapper over usePrintPreview(). Use the hook directly when you want full control over the page UI:
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. */}
<div
ref={ref}
aria-hidden
style={{
position: "absolute",
left: -99999,
width: contentWidth,
clipPath: "inset(50%)",
}}
>
{children}
</div>
{/* Your own page UI, computed from pages[]. */}
{pages.map((page) => (
<YourPageCard key={page.pageNumber} page={page} />
))}
{status === "measuring" && <Spinner />}
</>
);
}The hook returns:
| Field | Type | Notes |
|---|---|---|
ref | MutableRefObject<HTMLElement> | 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[]:
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.
- 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. - The walker reads computed styles + bounding rects and emits the same primitive list
exportToPDFwould. - The paginator slices that list into
Pageobjects withyOffsetandcontentHeight. Same code path, same behavior — includingbreak-inside: avoid,data-print-repeat,pageBreak="manual"markers, and the soft-card heuristic from RFC 0001. - 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 byyOffset - A footer band
- 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<PrintPreview>. - Repeat bands are placeholders. A continuation page that would re-render
<thead data-print-repeat>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
usePrintPreviewhook 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— the underlying export call. Preview shows you what this would produce.React API—usePDFExport,Printable,ExportButton. Compose these with<PrintPreview>for a full "edit / preview / download" UX.- Pagination — page breaks, headers, footers, repeat bands. The preview honors all of it.
- Sizing and alignment — how
pageContentWidthties your DOM width to the page. The preview's mounting container is sized with the same helper.