Short answer
To control page breaks in a PDF rendered from HTML, use CSS fragmentation properties: break-before: page to start an element on a new page, break-after: page to force a break after it, and break-inside: avoid to keep it on one page. Set paper size and margins with @page.
A browser on screen shows a document as one continuous column. A PDF has to cut that column into pages, and by default the cut lands wherever the page runs out of space: through the middle of a table row, between a heading and its first paragraph, or across a chart. CSS gives you a small set of properties to tell the renderer where it may and may not break.
We describe those properties as Chromium implements them, because Chromium is the engine inside Puppeteer, Browsershot and hosted services like htmltopdfapi.co, the HTML to PDF API we run and use in the examples. None of it is vendor-specific. The same CSS behaves the same way when you press Ctrl+P in Chrome.
How do I force a page break before or after an element?
Put break-before: page on the element that should start a new page, or break-after: page on the one that should end a page. In practice this lives on a class you apply to every chapter, or to each invoice when you render a batch of them into one file:
/* Every chapter starts on a new page */
.chapter {
break-before: page;
}
/* ...except the first, so there is no blank first page */
.chapter:first-child {
break-before: auto;
}
/* Generic helper */
.page-break {
break-after: page;
}The older properties page-break-before, page-break-after and page-break-inside still work in Chromium as aliases. page-break-before: always maps to break-before: page, and page-break-inside: avoid maps to break-inside: avoid. Use the modern names in new code and keep the legacy ones only if you also target old renderers such as wkhtmltopdf.
Chromium ignores forced breaks on elements outside normal block flow. When a break does nothing, the element is almost always a flex or grid item, floated, or absolutely positioned. A .chapter sitting inside a display: flex wrapper is a frequent culprit. See MDN on break-before for the full list of values.
How do I stop an element from splitting across pages?
break-inside: avoid asks the renderer to keep an element whole and push it to the next page if it does not fit in the space left. Chromium treats it as a preference. An element taller than a full page has to split somewhere, and it will.
.card,
figure,
blockquote,
.signature-block {
break-inside: avoid;
}
/* Keep a heading with the content that follows it */
h2, h3 {
break-after: avoid;
}break-after: avoid on headings targets the stranded heading, the "Payment terms" title that sits alone at the foot of page 3 while its text starts on page 4. Chromium honors avoid values on break-after less consistently than on break-inside, so the dependable fix is to wrap the heading and its first paragraph in one element with break-inside: avoid. MDN on break-inside lists support per value.
How do I set paper size and margins with @page?
The @page rule sets the page box. Chromium supports size (named sizes such as A4 or letter, orientation, or explicit dimensions) and margin, and the :first, :left and :right pseudo-classes for margins.
@page {
size: A4 portrait;
margin: 20mm 15mm;
}
@page :first {
margin-top: 40mm; /* extra space on the cover page */
}If you also pass paper size or margins as request options (paper_size, margin_top and so on), pick one source and stick to it. Chromium has a "prefer CSS page size" setting that some tools turn on and others leave off, so setting both is a reliable way to end up with margins nobody can explain. Margin boxes such as @bottom-center work in Chrome 131 and later but are ignored by older Chromium builds, so for page numbers we still recommend header and footer templates, explained in adding page numbers, headers and footers.
What are orphans and widows?
orphans sets the minimum number of lines of a paragraph that must stay at the bottom of a page before a break; widows sets the minimum that must carry over to the top of the next page. Chromium supports both for block text, and both default to 2. In contracts and long reports we set p { orphans: 3; widows: 3; }, which keeps a clause from leaving a lone line on either side of a break.
How do I repeat table headers on every page?
Put the header row in a real <thead>. Chromium automatically repeats <thead> at the top of each page a table spans, and repeats <tfoot> at the bottom. This only works with semantic table markup; a header row built from <div> elements or placed inside <tbody> will not repeat.
Without any break rules, a 40-row line-item table that spills onto page 2 can cut a row through its text, with the top half of the description on one page and the bottom half on the next. break-inside: avoid on tr stops that. A row taller than a whole page will still split.
thead { display: table-header-group; } /* default, but some resets change it */
tfoot { display: table-footer-group; }
tr {
break-inside: avoid;
}
/* Keep a short totals block with the end of the table */
.totals {
break-inside: avoid;
}Invoices with long line-item lists and reports built around data tables are where you will notice this first. If headers stop repeating, check the computed style of thead: a CSS reset or a "responsive tables" snippet that sets display: block on table parts switches repetition off.
Why do images, flexbox and grid layouts split badly?
An <img> is a replaced element, and Chromium will not slice one across pages. If it does not fit, it moves to the next page and leaves a gap behind it, sometimes half a page tall. Put images with captions in a <figure> with break-inside: avoid so the caption travels with the picture, and cap tall images with max-height so they fit on one page.
Fragmentation inside flex and grid layouts is less mature than in block layout. Break properties on flex or grid items can be ignored, and a tall container may split wherever the space runs out. When you need predictable breaks between items, switch the print layout to block flow (@media print { .grid { display: block; } }) or make each row its own block element with break-inside: avoid.
Fixed heights and hidden overflow cause the third kind of trouble. An ancestor with height: 100vh or overflow: hidden clips everything past the first page instead of letting it flow, so the PDF ends abruptly. Remove both in print styles.
Should I use print or screen media, and why are my backgrounds missing?
Chromium renders PDFs with print media rules by default. If your styles hide things in @media print, or your CSS framework ships a print stylesheet nobody has looked at, the PDF will not match the screen. On htmltopdfapi.co the css_media_type option picks print or screen. Our rule of thumb: use print, with break rules inside @media print, for documents designed as PDFs; use screen when you capture an existing page, such as a dashboard, as it looks in the browser.
Browsers drop background colors and images when printing, to save ink. Set print_background to true to keep them, and add -webkit-print-color-adjust: exact; print-color-adjust: exact; to elements whose backgrounds must always print, such as shaded table headers or colored status badges.
curl -X POST https://platform.htmltopdfapi.co/api/v1/pdf/generate \
-H "Authorization: Bearer $HTML_TO_PDF_API_KEY" \
-H "Content-Type: application/json" \
--output statement.pdf \
-d '{
"url": "https://example.com/statements/2026-10",
"paper_size": "a4",
"css_media_type": "print",
"print_background": true,
"margin_top": 20,
"margin_bottom": 20
}'How do I debug page breaks?
When a page splits in the wrong place, work down this list from the top.
- Open the page in Chrome, press Ctrl/Cmd+P and look at the print preview. It runs the same layout engine, so with matching paper size and margins its breaks closely match the PDF.
- In DevTools, open the Rendering panel and emulate the
printmedia type to inspect print styles live. - Check that the element you are styling is in normal block flow, not a flex item, grid item, float or absolutely positioned box.
- Look for
overflow: hidden, fixed heights and100vhon ancestors, which clip content instead of letting it flow. - Confirm tables use
<thead>,<tbody>and<tfoot>, and that no reset overrides their display values. - Check that page size and margins come from one place: either
@pageor the request options. - Add a temporary outline (
* { outline: 1px solid red; }) to see which box is too tall to fit. - If content renders late via JavaScript, make sure the renderer waits for it with
wait_untilorwait_for_selector, otherwise breaks are computed on an incomplete page.
Quick reference: page break properties in Chromium
Support notes describe Chromium's print engine as headless Chrome uses it. If a row here disagrees with what you see, trust Chrome's print preview.
| Property | Effect | Chromium support |
|---|---|---|
break-before: page | Start the element on a new page | Yes (block flow) |
break-after: page | Break after the element | Yes (block flow) |
break-inside: avoid | Keep the element on one page if it fits | Yes |
break-after: avoid | Avoid a break right after (e.g. headings) | Partial; wrap heading and content instead |
page-break-before/after/inside | Legacy aliases of the above | Yes |
@page { size; margin } | Paper size and page margins | Yes |
@page :first | Different margins on the first page | Yes |
orphans / widows | Minimum lines before/after a break | Yes |
thead / tfoot repetition | Table header/footer on every page | Yes |
@page margin boxes, counter(page) | Running headers and page numbers in CSS | Chrome 131+ only; header/footer templates work everywhere |
| Breaks inside flex/grid | Control splits between items | Limited; use block layout for print |
For the request flow behind these examples, see what an HTML to PDF API is. To try the CSS above against a real renderer, the getting started guide sets you up with a free-plan API key.
Frequently asked questions
How do I force a page break in HTML for PDF?
Add break-before: page to the element that should start a new page, or break-after: page to the element that should end one. The legacy page-break-before: always also works in Chromium.
Why is page-break-before not working in my PDF?
Forced breaks only apply to elements in normal block flow. If the element is a flex item, grid item, float or absolutely positioned, the break is ignored; switch that layout to block for print.
How do I stop table rows from splitting across PDF pages?
Apply tr { break-inside: avoid; }. Rows taller than a full page will still split, because there is nowhere else to put them.
How do I repeat a table header on every PDF page?
Put the header row inside a <thead> element. Chromium repeats thead at the top of every page the table spans, as long as no CSS overrides its display: table-header-group.
Can I add PDF page numbers with CSS alone?
In Chrome 131 and later, yes: @page margin boxes such as @bottom-center accept content: counter(page). Older Chromium builds skip those rules, so header and footer templates stay the dependable option. See page numbers, headers and footers.
Why are background colors missing from my PDF?
Browsers skip backgrounds when printing by default. Enable the print background option and set print-color-adjust: exact on elements whose backgrounds must print.
Related reading
Written by The htmltopdfapi.co team
Written by the engineers who build and run htmltopdfapi.co and maintain the official PHP/Laravel SDK. We ship PDF-heavy features — invoices, reports and certificates — every day, and every guide here comes from that work.