Skip to content

HTML to PDF page breaks: how to control where the page splits

A row cut in half, a card sliced across two pages, a heading stranded at the bottom: page breaks are the most common complaint about HTML to PDF. Here is the CSS that controls them, why it sometimes gets ignored, and how to get clean breaks on invoices, reports and tables.

You build a clean invoice or a multi-page report, convert it to PDF, and the layout falls apart at the page edges: a table row is sliced in half, a summary card straddles two pages, a section heading sits alone at the very bottom with its content overleaf. This is the single most common complaint about converting HTML to PDF, and the good news is that it is almost entirely controllable from CSS. The catch is that the rules only work in a renderer that actually implements them, which is the first thing to get right.

The short version

Page breaks in HTML to PDF are controlled by the CSS fragmentation properties: break-inside: avoid to stop an element being split, break-before and break-after to force or prevent a break at a boundary, and orphans and widows to keep stray lines together. These only take effect when the PDF is produced by an engine that supports paged media, a real browser like Chromium does, and pure-code engines vary. If your breaks are being ignored entirely, the usual cause is either a renderer that does not implement these properties or a flex or grid container that changes how fragmentation applies. Fix the CSS first, and switch renderers only if the CSS is correct and still ignored.

The properties that actually control breaks

Modern CSS replaced the old page-break-* names with the shorter break-* family, and browsers still accept both, so you will see either in real code. The four you reach for constantly are these.

break-inside: avoid is the workhorse. Put it on any element you never want split across a page boundary, a table row, an invoice line item, a card, a figure with its caption, and the renderer will push the whole element to the next page rather than cut it. This one property solves most "my rows are chopped in half" problems on its own.

.line-item,
tr,
.summary-card,
figure {
  break-inside: avoid;
}

break-before: page and break-after: page force a break at a specific point. The classic use is starting each major section or each invoice on a fresh page: put break-before: page on the section wrapper. There is also break-before: avoid, which is how you keep a heading glued to the content beneath it so it never ends up stranded at the foot of a page.

.invoice + .invoice { break-before: page; }
h2 { break-after: avoid; }   /* keep a heading with its first paragraph */

Finally, orphans and widows control how many lines of a paragraph may be left alone at the bottom or top of a page. Setting both to 3 means a paragraph will never leave fewer than three lines on either side of a break, which quietly removes the ugly single dangling line.

p { orphans: 3; widows: 3; }

Repeating table headers on every page

A long table that flows across several pages needs its header row repeated at the top of each page, otherwise page two onward is a wall of numbers with no column labels. The fix is structural, not a special property: put your header cells inside a real <thead> and your body rows inside <tbody>. A browser-based renderer will automatically repeat the <thead> on each page the table spans. Many teams miss this simply because their markup dumps every row into one flat table with no <thead>, so there is nothing for the engine to repeat. The same markup pays off twice: a real <thead> with proper scope attributes is also what lets a screen reader announce the right column header with each cell, which is covered in the guide to producing an accessible, tagged PDF from HTML.

<table>
  <thead>
    <tr><th>Date</th><th>Description</th><th>Amount</th></tr>
  </thead>
  <tbody>
    <!-- rows; each <tr> can carry break-inside: avoid -->
  </tbody>
</table>

Why your page-break CSS is being ignored

When the properties above do nothing, the problem is almost never your CSS syntax. It is one of three things.

The first and most common is the renderer. Fragmentation is one of the least uniformly implemented corners of CSS. Chromium implements break-inside: avoid and repeating table headers well, which is why a browser-based conversion tends to behave. Older or lighter engines do not. wkhtmltopdf, still lurking under many older stacks, has long-standing, unfixable bugs where break-inside: avoid is partially or fully ignored, and it was archived in 2023 so those bugs will never be patched; the wkhtmltopdf alternative page covers that in full. dompdf and similar pure-code libraries support some break rules but not all, and their table pagination is weaker. If your CSS is correct and breaks are still ignored, you are fighting the engine, not your stylesheet.

The second cause is flex and grid containers. The CSS fragmentation spec around flex and grid is complicated, and browsers historically did not fragment inside a flex container the way you expect, so a tall display: flex column could refuse to break cleanly or could ignore break-inside on its children. If a specific block will not break properly, try making its wrapper a plain block rather than a flex or grid container for the print output, which often restores predictable behaviour.

The third is fixed heights and overflow. An element with a hard height or overflow: hidden can clip its own content at a page boundary instead of flowing onto the next page, so content simply vanishes rather than breaking. For anything that should paginate, let the height be automatic.

Use a print stylesheet, and preview it in the browser

Most HTML to PDF engines render your page as if for print, so your @media print rules apply and your screen-only rules can get in the way. Keep the break rules in a print context so they do not affect the on-screen view, and give the page a sensible paged size and margin with the @page rule.

@page { size: Letter; margin: 18mm 16mm; }

@media print {
  .line-item, tr { break-inside: avoid; }
  .invoice + .invoice { break-before: page; }
  .no-print { display: none; }
}

You do not have to generate a PDF on every iteration to see how breaks will fall. In Chrome, open the print dialog (Ctrl or Cmd P) and the preview paginates your page using the exact same engine a Chromium-based converter uses, so what you see there is what you get in the PDF. That feedback loop turns page-break debugging from guesswork into a few seconds per change.

How this looks through an API

None of this changes when you render server side. A browser-based rendering API is running the same Chromium print path, so your break-inside, @page and <thead> rules are honoured exactly as the print preview showed. You send the URL or HTML, and the pagination you tuned in the browser comes back in the file.

curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -F url=https://app.example.com/invoices/8842 \
  -F format=Letter \
  -F print_background=true \
  -F archive=true

Because the engine is a real, current browser, the fragmentation properties behave, which is exactly why teams move off older converters when clean page breaks start to matter. The full parameter list is on the PDF generator API page, and if you are converting a whole page rather than a fragment, convert HTML to PDF walks through the basics. When these are invoices, keeping the source markup clean also makes life easier downstream for anyone who later needs to pull the line items back out of a PDF invoice into a spreadsheet.

A quick checklist

SymptomFix
Table rows or cards split across pagesbreak-inside: avoid on the row or card
Column headers missing after page oneWrap header rows in <thead>, body in <tbody>
Heading stranded at the bottom of a pagebreak-after: avoid on the heading
Each record should start a new pagebreak-before: page on the wrapper
Single dangling lines at page edgesorphans: 3; widows: 3;
Content clipped or vanishing at a breakRemove fixed height and overflow: hidden
All break CSS ignoredRenderer does not support fragmentation; use a browser-based engine

Get the CSS right first, preview it in Chrome's print dialog to confirm, and only then blame the converter. Nine times out of ten a break-inside: avoid in the right place and a real <thead> fixes the whole thing. The tenth time, you have an engine that does not implement paged media, and the fix is to render in a real browser instead.

Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API