Guides

Page breaks in HTML to PDF: the CSS that actually works

Rows split across pages, headings orphaned at the bottom, cards cut in half. The four CSS properties that fix it in Chromium-based renderers, with the traps.

M Mikel Rougstone · 31 August 2026 · 4 min read
Page breaks in HTML to PDF: the CSS that actually works

Page breaks are the first thing that looks wrong when a web page becomes a document. The browser laid it out as one infinite column; the PDF has to cut that column into A4 slices, and it cuts wherever the slice ends unless you say otherwise. Four CSS properties fix most of it, and three common patterns quietly stop them from working.

The four properties

  • break-inside: avoid on anything that must not be split: table rows, cards, a figure with its

caption, a signature block.

  • break-before: page on an element that should start a new page — a chapter heading, an appendix.
  • break-after: avoid on headings, so a heading never sits alone at the bottom with its paragraph

overleaf.

  • orphans and widows — the minimum number of lines allowed at the bottom and top of a page.

Three is a sensible value for body text.

h2, h3 { break-after: avoid; }
tr, .card, figure { break-inside: avoid; }
.appendix { break-before: page; }
p { orphans: 3; widows: 3; }

The older page-break-inside / page-break-before names still work — Chromium treats them as aliases — but the unprefixed versions are what current specifications use.

Tables that span pages

Two rules, and people usually remember only the first:

thead { display: table-header-group; }   /* repeat the header on every page */
tfoot { display: table-footer-group; }   /* repeat totals, if you want them repeated */
tr    { break-inside: avoid; }           /* never cut a row in half */

Without the header group, page two of a twelve-page statement is a wall of numbers with no column labels. Without break-inside on rows, a row with a two-line description gets sliced across the page boundary.

If a table row is taller than a page — a long terms-and-conditions cell, for instance — no property
saves you. The renderer must break somewhere. Split the content into multiple rows.

What silently breaks page breaks

1. overflow: hidden on an ancestor. A renderer cannot break inside a box that clips its content, so it either overflows the page or squashes. This is the single most common cause, and it usually arrives from a CSS framework's container class.

2. Flexbox and grid containers. Children of a flex row are laid out as a unit. Put break-inside: avoid on the row itself, not on the cells, and be aware that a tall flex container may refuse to break at all. For paged documents, plain block layout and tables are more predictable.

3. Fixed heights. height: 100vh means nothing sensible on paper. Anything with a fixed height that exceeds the printable area will clip. Use min-height or nothing at all.

A print stylesheet, not a screen one

Everything above belongs in @media print, or in a stylesheet loaded only for PDF generation:

@media print {
  nav, .cookie-banner, .no-print { display: none; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 90%; color: #666; }
  .card { box-shadow: none; border: 1px solid #ddd; }
  body { font-size: 11pt; line-height: 1.5; }
}

Three things worth doing there: hide navigation and banners, make link targets visible if the document will be printed on paper, and drop shadows — they render as grey mush and inflate file size.

A renderer that emulates print media applies these automatically. If your output looks like the screen version, that emulation is off — in raw Puppeteer it is page.emulateMedia({media: 'print'}); in PDFGeny it is always on.

Page size and margins belong to the renderer

@page { size: A4; margin: 18mm } works, but the renderer's own options override it and are easier to change per document:

{"html": "…", "format": "A4", "margin": {"top": "18mm", "right": "14mm", "bottom": "20mm", "left": "14mm"}}

Leave room at the bottom for the footer. A common mistake is a 10 mm bottom margin with a footer template that needs 15 mm — the page number overlaps the last line of text.

Headers, footers and page numbers

These are not CSS. Chromium takes separate header and footer templates, and that is where page numbers live:

{"footer_html": "Page {{page}} of {{pages}} · Invoice INV-2026-014"}

Two traps: the templates inherit no styles from the document, so set font size inline; and content outside the page margin is clipped, so a footer needs bottom margin to sit in.

Testing page breaks properly

Page breaks only appear with enough content to fill a page. Three habits catch nearly everything:

1. Render your longest realistic document, not a two-row fixture. The customer with fourteen line items and a five-line address is the one who finds the bug. 2. Check the last page. A lone footer row or an orphaned signature block is the classic leftover. 3. Try the boundary. Add content until a table crosses a page break, then remove one row. Both states should look right.

You can do this in the browser without generating anything: open the print preview and scroll. It is not identical to the renderer's output — see why the PDF differs from the print dialog — but page break behaviour is the same engine, so it is a fast first check.

Try it

Paste a document with a long table into the HTML to PDF converter, download the result, and watch where it breaks. Then add thead { display: table-header-group } and do it again — the difference is immediate.

M

Mikel Rougstone

Founder, PDFGeny

I build and run PDFGeny — the API, the rendering fleet and the template catalog. Most of what I write here comes from something that broke in production first.