PDFreact-print-pdf
DocsBrowser-only alpha

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.

Edit this page

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.

Live <PrintPreview> · A4Open in new tab
Pages are rendered inline as scaled DOM — no PDF round-trip. The same paginator drives 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.

<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

PropTypeDefaultNotes
formatPageFormat"A4"Same as exportToPDF. Accepts named formats or [width_mm, height_mm].
orientation"portrait" | "landscape""portrait"
marginMargin0String / 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) => ReactElementRenders per page in the top margin band.
footer(ctx) => ReactElementRenders per page in the bottom margin band.
headerHeightnumber0CSS px reserved at the top. Set this when you pass header.
footerHeightnumber0CSS px reserved at the bottom.
scalenumber0.85Visual zoom. Doesn't affect pagination math.
gapnumber24CSS px between page cards (scaled).
pageBackgroundCSSProperties["background"]"white"Background of each page card.
showPageNumbersbooleantrueTiny 1 / N badge in the bottom right of each card.
className, styleApplied to the outer scroll container.
childrenReactNodeMust 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:

FieldTypeNotes
refMutableRefObject<HTMLElement>Attach to your off-screen mount.
pagesPreviewPage[]One entry per computed page; carries pageNumber, totalPages, yOffset, margin, contentHeight, context, hasRepeatBand.
totalPagesnumberConvenience accessor.
contentWidthnumberCSS px width of the page content area (page width minus L+R margins).
pageWidth, pageHeightnumberCSS px of the paper.
status"idle" | "measuring" | "ready" | "error"Render gating.
errorError | nullLast thrown error, cleared on next successful measure.
recompute() => voidManual 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.

  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 <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 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):

StageCost
Mount + first layoutone React commit
Walk + paginate~5–15ms on a modern laptop
Snapshot via outerHTML~1–3ms per measure
Per-card DOM clipcheap; 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.

  • exportToPDF — the underlying export call. Preview shows you what this would produce.
  • React APIusePDFExport, 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 pageContentWidth ties your DOM width to the page. The preview's mounting container is sized with the same helper.

On this page