Skip to content
CastPDF

HTML to PDF page break not working: causes and fixes

A page break in HTML to PDF is usually ignored because the element sits inside a flex or grid container, floats, is absolutely positioned, is inline, or lives in a clipped box. Make it a plain block in normal flow, then use break-before, break-after and break-inside. Our HTML to PDF API honours them.

  • Updated October 2026
  • 100 free PDFs a month

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> with break-before does nothing. The rule needs a block-level box such as a <div>, <section> or heading.
  • A clipped container. A parent with a fixed height and overflow: hidden or overflow: auto keeps 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

  1. 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.
  2. Flatten the layout for print. In an @media print block, set those parents to display: block, remove floats, and reset absolute positioning to position: static.
  3. Remove fixed heights and clipping. Set height: auto and overflow: visible on the wrapper, and on <html> and <body> if your app shell sets them.
  4. Put the rule on a block element. Apply break-before: page to a heading or a section, never to a span. For legacy engines add page-break-before: always next to it.
  5. Protect blocks that must stay whole. Add break-inside: avoid or the keep-together class to totals, signatures and figures with captions.
  6. Render in print mode and check the page count. Send "mode": "print" and read the x-pages response 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:

report-print.css
@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:

report.html
<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
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.pdf

Open 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.

content_lost response
{
  "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.

FAQ

Common questions

Why does break-before: page not work inside a flex container?

Forced breaks between flex or grid items are handled unevenly by print engines and are often dropped. Set the container to display: block in your print styles. The children then flow as normal blocks and the break applies.

Is page-break-before still supported?

Yes. Modern engines treat page-break-before, page-break-after and page-break-inside as aliases of the newer break properties. You can write both forms side by side.

Can I force a page break on a span or a link?

No. Break properties apply to block-level boxes in normal flow. Wrap the content in a div or section, or put the rule on the nearest heading.

What do orphans and widows do in a PDF?

They set the minimum number of paragraph lines left at the bottom of a page and carried to the top of the next. A value of 2 or 3 avoids single stray lines.

What does the content_lost error mean?

It means print mode found content that would be cut off, so no PDF was made. The usual causes are a long table inside a table cell or a rowspan cell taller than a page. Restructure that part, or render it in fast mode.

Why do I get an empty page between two sections?

Two forced breaks meet, usually break-after on one section and break-before on the next. Keep only one of them, and turn it off for the first or last section.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.