The problem: the break rule does nothing
You wrote break-before: page on every chapter heading of a report. In the PDF, chapter two starts halfway down page three, right after the last paragraph of chapter one. Or you marked a signature box with break-inside: avoid, and the signature line still lands on one page while the name lands on the next.
The CSS looks correct, and it often is. The trouble is where the element sits in the layout. Break properties only apply to certain boxes, in certain places. When the element falls outside those rules, the engine skips the property without a warning.
This is the most common complaint about generated documents. Line items split mid-row, headings get stranded at the bottom of a page, and totals drift away from the table they sum. The fixes below are short, and you can apply them one at a time.
Why page breaks are ignored
The CSS fragmentation rules describe breaks between block-level boxes that flow one after another down the page. Anything that takes a box out of that simple flow can make a break rule meaningless. These are the usual suspects, roughly in order of how often we see them.
- A flex or grid parent. Page layouts built with a CSS framework wrap everything in flex or grid containers. Forced breaks between flex items or grid items are handled unevenly across engines and versions, and are often dropped.
- A float. Break properties apply to boxes in normal flow. A floated sidebar or a floated image ignores them, and so can content that wraps around it.
- Absolute or fixed positioning. A positioned element is lifted out of the flow, so the engine has no place in the flow to break before or after it.
- An inline element. A
<span>or an<a>withbreak-beforedoes nothing. The rule needs a block-level box such as a<div>,<section>or heading. - A clipped container. A parent with a fixed
heightandoverflow: hiddenoroverflow: autokeeps its content inside one box. The content is cut off, or scrolls, instead of flowing onto the next page.
Two smaller causes are worth a quick check too. Some styles set height: 100% on <html> and <body> for a full-screen app shell, which has the same effect as a clipped container. And a break rule inside a @media screen block never reaches the print layout.
The fix, step by step
- Find the element that should break. Open the HTML in a desktop browser and inspect the heading or block. Walk up its parents and note any flex, grid, float, position or overflow styles.
- Flatten the layout for print. In an
@media printblock, set those parents todisplay: block, remove floats, and reset absolute positioning toposition: static. - Remove fixed heights and clipping. Set
height: autoandoverflow: visibleon the wrapper, and on<html>and<body>if your app shell sets them. - Put the rule on a block element. Apply
break-before: pageto a heading or a section, never to a span. For legacy engines addpage-break-before: alwaysnext to it. - Protect blocks that must stay whole. Add
break-inside: avoidor thekeep-togetherclass to totals, signatures and figures with captions. - Render in print mode and check the page count. Send
"mode": "print"and read thex-pagesresponse header. A sudden jump in pages often means a forced break fired twice.
Here is a print stylesheet for a report whose screen version uses a two-column grid. It turns the grid back into a single column on paper, so the break rules can do their job:
@media print {
.page-shell { display: block; height: auto; overflow: visible; }
.sidebar { float: none; position: static; width: auto; }
html, body { height: auto; overflow: visible; }
section.chapter { break-before: page; page-break-before: always; }
section.chapter:first-of-type { break-before: auto; page-break-before: auto; }
h2, h3 { break-after: avoid; page-break-after: avoid; }
figure, .totals, .signature { break-inside: avoid; page-break-inside: avoid; }
p { orphans: 3; widows: 3; }
}The markup stays simple. Each chapter is a <section>, a plain block. The sign-off area uses the keep-together class, which CastPDF already treats as break-inside: avoid:
<div class="page-shell">
<section class="chapter">
<h2>Summary</h2>
<p>Revenue grew in every region this quarter.</p>
</section>
<section class="chapter">
<h2>Regional detail</h2>
<figure>
<img src="https://cdn.example.com/charts/regions.png" alt="Revenue by region">
<figcaption>Revenue by region, in thousands</figcaption>
</figure>
</section>
<div class="keep-together signature">
<p>Approved by</p>
<p>Finance Director</p>
</div>
</div>Send both in one request. Raw HTML renders in fast mode by default, so ask for print mode explicitly:
curl https://api.castpdf.com/v1/pdf \
-H "Authorization: Bearer $CASTPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<section class=\"chapter\"><h2>Summary</h2><p>Revenue grew.</p></section><section class=\"chapter\"><h2>Regional detail</h2><p>North led.</p></section>",
"css": "section.chapter { break-before: page; } section.chapter:first-of-type { break-before: auto; }",
"mode": "print",
"format": "A4"
}' \
--dump-header headers.txt \
--output report.pdfOpen headers.txt and look for x-pages: 2. One page per chapter means the forced break worked, and the first chapter did not leave an empty page in front of it.
Variations and edge cases
Old property names
The page-break-before, page-break-after and page-break-inside properties are the older spelling. Modern engines map them to the new break-* properties, so either works. Writing both does no harm, and it helps if the same CSS also feeds an older converter.
Lonely lines at the top or bottom of a page
A single line of a paragraph left at the bottom of a page is an orphan. A single line pushed to the top of the next page is a widow. Set orphans and widows to 2 or 3 on paragraphs, and the engine moves extra lines across so neither page holds a stray line.
A heading stranded at the foot of a page
Add break-after: avoid to headings. The engine then prefers to move the heading down with its first paragraph. It is a preference, not a promise: if the next block is too tall, the heading may still end a page.
Long tables and rows taller than a page
CastPDF adds break-inside: avoid to every table row, so normal rows always move to the next page whole. In print mode a row taller than a full page is split across pages instead, so its text is not lost. The repeating table header guide shows how to keep the column names on every continued page.
Blank pages between sections
Two forced breaks in a row create an empty page. It happens when a section has break-after: page and the next one has break-before: page. Pick one side and use it everywhere, and switch it off for the first or last section.
How CastPDF handles it
CastPDF renders in Chromium. In print mode, Paged.js paginates the document first, and our handlers fill the gaps it leaves. Break rules are honoured when they are written in inline style attributes, on elements with other inline styles, and in a <style> or stylesheet link placed inside <body>. Paged.js on its own would miss those.
Every document gets tr, img, figure, .keep-together { break-inside: avoid; } in both modes. In print mode, an image, canvas or SVG taller than a page is scaled down to fit one page. A keep-together block taller than a page is split rather than cut.
When print mode still finds content that would be cut off, the request fails with content_lost (HTTP 422) instead of handing you a PDF with missing text. Two layouts trigger it: a table nested in a cell that grows taller than a page, and a rowspan cell taller than a page. Move the inner table out of the cell, or give each line its own row.
{
"error": {
"code": "content_lost",
"message": "Part of the document could not fit on a page and would be cut off.",
"docs_url": "https://castpdf.com/docs/errors#content_lost"
}
}Once breaks behave, add page numbers in the footer and check that your fonts load on the server. The print CSS reference lists every rule the renderer adds. If you run Chrome yourself today, the CastPDF vs Puppeteer comparison explains what moves to the API.