Page breaks in HTML-to-PDF: the complete CSS guide

Updated July 2026 · applies to Chromium-based rendering (Puppeteer, Playwright, NippyPDF)

When HTML becomes a PDF, the browser’s fragmentation engine decides where each page ends. You control it with three properties, one at-rule, and — most importantly — by not using layouts that disable fragmentation entirely. This guide covers all of it, with the gotchas that cost people afternoons.

The core properties: break-before, break-after, break-inside

/* The three fragmentation properties (modern names + legacy aliases) */

.chapter   { break-before: page; }   /* start on a fresh page   (page-break-before: always) */
.appendix  { break-after: page; }    /* force a break after     (page-break-after: always)  */
.invoice-block { break-inside: avoid; } /* never split this box (page-break-inside: avoid)  */

/* Keep headings with the content that follows them */
h2, h3 { break-after: avoid; }

/* Typography at breaks: never strand single lines */
p { orphans: 3; widows: 3; }

Paper size and margins: @page

/* The @page rule controls the paper itself */
@page {
  size: A4;            /* or: letter, A5, 210mm 297mm, landscape */
  margin: 20mm 16mm;   /* page margins live here, not on <body> */
}

@page :first {
  margin-top: 40mm;    /* roomier first page for a letterhead */
}

/* Print-only adjustments */
@media print {
  nav, .no-print { display: none; }
  a { color: inherit; text-decoration: none; }
  body { print-color-adjust: exact; } /* keep background colours in the PDF */
}

Tables that span pages

/* Multi-page tables: use real <thead>/<tfoot> and they repeat automatically */
table { border-collapse: collapse; }
thead { display: table-header-group; }  /* default for a real <thead>, explicit here */
tfoot { display: table-footer-group; }
tr    { break-inside: avoid; }          /* rows move whole to the next page */

This is where Chromium leaves legacy tools behind: real repeating <thead>, whole rows moved cleanly to the next page. (If you’re seeing rows split mid-row, you’re probably on wkhtmltopdf — that engine needs different workarounds.)

The gotchas that silently break breaking

/* THE GOTCHAS — layouts that silently turn off fragmentation.
   Breaks do not happen inside these; content overflows or clips instead. */

/* 1. Flex/grid: fine for headers/footers, wrong for long printable flow */
.report { display: flex; }          /* content in flex items won't fragment */
@media print { .report { display: block; } }  /* fix: linearize for print */

/* 2. Overflow: a scroll container is an unbreakable box */
.card { overflow: hidden; }         /* also auto/scroll — blocks page breaks */
@media print { .card { overflow: visible; } }

/* 3. Fixed viewport heights make no sense on paper */
.page { height: 100vh; }
@media print { .page { height: auto; } }

/* 4. position: fixed/absolute content doesn't flow — it repeats or overlaps.
      For running headers/footers, use your renderer's header/footer feature
      (e.g. the header_html/footer_html options in a PDF API) instead. */

The rule of thumb: screen layout and print flow are different problems. Keep flex and grid for on-screen chrome, and let the document you print be boring block layout. A five-line @media print block converting containers back to display: block; overflow: visible fixes most “breaks don’t work” reports.

Debugging workflow

  1. Chrome DevTools → Rendering panel → Emulate CSS media type: print to see print styles live.
  2. Ctrl/Cmd+P print preview to check actual pagination — Chromium PDF APIs use this same pipeline.
  3. Outline suspect containers (* { outline: 1px solid rgba(255,0,0,.2) } in print media) to spot unbreakable boxes.

FAQ

page-break-inside vs break-inside — which should I use?

break-inside is the modern property; page-break-inside is the legacy alias. Chromium treats the old names as aliases of the new ones, so either works there. Use the modern break-* names, and add the page-break-* aliases only if you must also support legacy engines like wkhtmltopdf.

Why is break-inside: avoid not working?

Nine times out of ten an ancestor is preventing fragmentation: content inside flex or grid items, floats, position: absolute, or any element with overflow other than visible will not fragment normally. Restructure the printable flow as plain block layout (a print stylesheet can do this with display overrides) and the same declaration starts working.

How do I preview page breaks without generating a PDF each time?

Chrome DevTools → the Rendering panel → "Emulate CSS media type: print" shows print styles live, and Ctrl/Cmd+P print preview shows the actual pagination. Chromium-based PDF APIs use this same pipeline, so what print preview shows is what you get.

Can I keep a heading attached to the content after it?

Yes: h2, h3 { break-after: avoid; } in a Chromium-based renderer. This is one of the clearest upgrades over wkhtmltopdf, whose engine never supported avoid values on break-after.

Write the CSS, skip the rendering infrastructure. NippyPDF is a managed Chromium rendering core behind a single POST /v1/pdf — no browser fleet, no page-break roulette, around $0.003/document.

Try the live demoJoin the waitlistGenerate the PDF itself: HTML to PDF in Node.js →