- HTML to PDF APIConvert HTML and URLs into PDFsExtract PDF Form Data APIEasily extract data from PDFsWatermark PDF APIAdd custom watermarks to PDFsProtect PDF APISecure your PDFs with passwordCompress PDF APIReduce file size without losing qualityFlatten PDF APIFlatten PDFs to make form permanentDigital Signature APISend documents for signing
- API DocumentationAPI DocumentationFull REST API referenceNodeJS SDKClient library for Node.jsJava SDKClient library for JavaC# SDKClient library for C#PHP SDKClient library for PHPPython SDKClient library for Python
- Pricing
- Contact Us

HTML to PDF Page Breaks: How to Control Them with CSS
Force, avoid, and debug page breaks when converting HTML to PDF. Covers break-before and break-inside, orphans and widows, repeating table headers, and why Chromium ignores some break rules.
A web page is one long, continuous canvas. A PDF is a stack of fixed-size pages. When you convert HTML to PDF, something has to decide where each page ends, and if you leave that decision to the renderer you get the familiar results: a table row sliced in half, a heading stranded at the bottom of a page, a single line of a paragraph orphaned at the top of the next one, or an invoice total pushed onto a page of its own.
You control all of this with a small set of CSS properties. This guide covers how page breaks work in Chromium, the engine behind Chrome, Puppeteer, Playwright, and most HTML to PDF APIs, which properties and values it actually honors, and the layout patterns that silently cause break rules to be ignored.
Key takeaways
- Use
break-before: pageto force a new page andbreak-inside: avoidto keep a block in one piece. The legacypage-break-*properties still work as aliases. - Put table headers in
<thead>and Chromium repeats them on every page. Addbreak-inside: avoidto rows to stop them splitting. - Break rules only apply to block-level boxes in normal flow. Flex and grid parents, floats, fixed heights, and scroll containers are the usual reasons they get ignored.
- Debug with Chrome print preview, not just DevTools media emulation, because only print preview shows real pagination.
How pagination works in Chromium
When Chromium prints, it lays the document out into a sequence of page-sized containers called fragmentainers. The process of splitting content across them is called fragmentation, defined in the CSS Fragmentation specification. As layout reaches the bottom of a page, the engine looks for a break opportunity: between two sibling blocks, between lines of text, or between table rows. It then picks the best one based on the break properties you set.
Two kinds of breaks matter. A forced break happens because you asked for one, such as break-before: page, and always wins. An unforced break happens because content ran out of room. The avoid values do not prevent unforced breaks outright. They tell the engine to prefer a different break opportunity if one exists. If a block marked break-inside: avoid is taller than a whole page, there is no better option, so it breaks anyway.
Some content is monolithic, meaning it can never be split. Images, canvases, iframes, and scroll containers fall into this category. A tall image moves to the next page as a unit rather than splitting at a clean boundary, so keep images and charts shorter than the printable page height.
The break properties: modern vs legacy
CSS has three modern break properties, break-before, break-after, and break-inside. They replace the older page-break-before, page-break-after, and page-break-inside, which Chromium still supports as aliases.
1/* Modern syntax (preferred) */2.chapter { break-before: page; }3.card { break-inside: avoid; }4h2, h3 { break-after: avoid; }56/* Legacy syntax, still supported as aliases */7.chapter { page-break-before: always; }8.card { page-break-inside: avoid; }9h2, h3 { page-break-after: avoid; }| Value | Legacy equivalent | Effect in a PDF |
|---|---|---|
break-before/after: page | page-break-*: always | Forces a new page |
break-before/after: left | right | page-break-*: left | right | Forces a new page. Treat it as page for single-sided output |
break-before/after: avoid | page-break-*: avoid | Prefers not to break at this edge |
break-inside: avoid | page-break-inside: avoid | Prefers to keep the element on one page |
avoid-page | None | Same as avoid when printing |
column, avoid-column | None | Only affects multi-column layouts, not pages |
Chromium rebuilt its block fragmentation engine in 2022 (LayoutNG, shipped around Chrome 102). Older Chrome builds, and tools that bundle them, ignored several of these rules, most notably break-after: avoidand breaks inside flex and grid containers. If advice you find online says a property "does not work in Chrome", check its date. Modern Chromium, which is what current Puppeteer versions and PDFGate use, handles far more than it used to.
Forcing a page break
The most common use of a forced break is one document per page: one invoice, one certificate, or one report section per page, all rendered from a single HTML file.
1/* One invoice per page when you render many in a single document */2.invoice {3 break-after: page;4}56/* Do not leave a blank page after the last one */7.invoice:last-child {8 break-after: auto;9}Use break-after on a repeating element and reset it on the last one, or use break-before and skip the first one with .invoice + .invoice. Either approach avoids a trailing blank page, which is one of the most common complaints in generated PDFs. Margins on an element that follows a forced break are kept, while margins at the top of a page after a natural break are usually truncated, so do not rely on margin-top for spacing at the top of a page.
If you are generating invoices this way, our guide to automating invoice generation with HTML to PDF covers the full workflow from template to delivery.
Avoiding breaks inside cards, figures, and rows
Most layout problems in PDFs come from unforced breaks landing in the wrong place. Mark anything that reads as a single unit with break-inside: avoid.
1/* Keep self-contained blocks in one piece */2.card,3figure,4blockquote,5pre,6tr {7 break-inside: avoid;8}910/* Keep headings attached to the content that follows */11h1, h2, h3, h4 {12 break-after: avoid;13}1415/* Belt and braces: group a heading with its first block */16.keep-together {17 break-inside: avoid;18}Be selective. If you add break-inside: avoid to large wrappers such as main or a whole section, every one of them jumps to a new page and leaves large blank gaps. Apply it to the smallest element that must stay intact.
Keeping headings with the next paragraph
A heading at the very bottom of a page with its content on the next page is the classic pagination defect. break-after: avoid on headings tells Chromium not to break between the heading and whatever follows. It is a preference rather than a guarantee, so for headings that must never be stranded, wrap the heading and its first block in a container and avoid breaks inside that container instead.
1<section class="keep-together">2 <h2>Payment terms</h2>3 <p>Invoices are due within 30 days of the issue date...</p>4</section>Orphans and widows
Inside a paragraph, the break opportunities are between lines. orphans sets the minimum number of lines left at the bottom of a page, and widows sets the minimum number carried to the top of the next page. Chromium supports both and defaults each to 2.
1p {2 orphans: 3; /* at least 3 lines at the bottom of a page */3 widows: 3; /* at least 3 lines at the top of the next page */4}These properties only apply to lines of text inside a block container. They have no effect on a list of separate div elements, where you would use break-inside: avoid on a wrapper instead.
Tables: repeating headers and splitting rows
Long tables are where HTML to PDF output most often looks unprofessional. Chromium handles them well once the markup is correct.
1<table class="line-items">2 <thead>3 <tr><th>Item</th><th>Qty</th><th>Price</th></tr>4 </thead>5 <tbody>6 <tr><td>Consulting</td><td>10</td><td>$1,500.00</td></tr>7 <!-- hundreds more rows -->8 </tbody>9 <tfoot>10 <tr><td colspan="3">Continued on next page</td></tr>11 </tfoot>12</table>1.line-items thead { display: table-header-group; }2.line-items tfoot { display: table-footer-group; }3.line-items tr { break-inside: avoid; }- Repeating headers: a
<thead>hasdisplay: table-header-groupby default, and Chromium repeats it at the top of every page the table spans. A header row placed inside<tbody>, or a header built fromdivelements, will not repeat. Declaring the display value explicitly guards against a CSS reset that changes it. - Repeating footers: a
<tfoot>(table-footer-group) repeats at the bottom of each page in current Chromium. Use it for "continued" notes, not for grand totals, which should appear once. Put totals in a separate block after the table. - Splitting rows: by default a tall row can split across two pages, with half its cell content on each. Add
break-inside: avoidtotrto move the whole row instead. - Don't wrap tables in flex or grid for print: a table inside a flex item still fragments, but keeping the direct parent a plain block gives the most predictable header repetition and row placement.
Why your page break rules are ignored
When a break rule does nothing, the cause is almost always the layout context of the element rather than the rule itself. Work through these in order.
- The element is not a block in normal flow. Break properties apply to block-level boxes, table rows, and flex and grid items. They have no effect on
display: inlineelements such as a barespan, and none on floated or absolutely positioned elements, because those are taken out of normal flow. - The parent is a flex or grid container. Modern Chromium can fragment flex and grid layouts, but forced breaks between their items are less predictable than in block flow, especially in row-direction flex where items sit side by side. Older Chromium ignored them completely. Switching the wrapper to
display: blockin print is the reliable fix. - A fixed height or a scroll container. Many app layouts set
height: 100vhandoverflow: autoon a main panel so that it scrolls inside the window. A scroll container is monolithic, so only its visible area is printed, and you get a PDF with a single clipped page. A fixed height withoverflow: hiddenclips anything that does not fit. Reset both for print. position: fixedelements. Chromium repeats fixed-position elements on every page. That can be useful for a watermark, but a fixed navbar or chat widget will cover content on every page. Hide them for print.- Your
@media printrules never apply. Tools that render a URL with screen media, which many do so the PDF matches what users see, never load your print stylesheet. Put break rules outside media queries or render with print media. - The element is taller than a page.
break-inside: avoidcannot be satisfied, so the element breaks where it must.
A print reset along these lines fixes most single-page-app layouts:
1@media print {2 html, body {3 height: auto;4 overflow: visible;5 }67 .layout,8 .layout > main {9 display: block; /* flex/grid parents make forced breaks less predictable */10 height: auto; /* fixed heights clip content instead of flowing it */11 overflow: visible;12 }1314 .sidebar,15 .cookie-banner {16 display: none;17 }18}The @page rule: size, margins, and named pages
The @page rule describes the page box itself: its size and margin. Chromium also supports named pages, where the page property assigns an element to a named @page rule and a change of page name forces a new page.
1@page {2 margin: 20mm 15mm;3}45.appendix {6 page: wide; /* named page, starts on a new page */7}89@page wide {10 margin: 10mm;11}In practice, when you generate PDFs programmatically, page size and margins are usually set by the rendering call rather than by CSS. Puppeteer's page.pdf() uses its own format and ignores @page size unless you opt in with preferCSSPageSize. With PDFGate, set page size and margins through the API parameters, as described in our guide to page size and margins, and leave CSS to handle what goes on each page. Leave enough margin for any header or footer templates, covered in our PDF header and footer guide.
Debugging page breaks in Chrome
Because PDFGate and most converters use Chromium, desktop Chrome is the fastest place to debug.
- Print preview (
Ctrl+PorCmd+P) shows real pagination. Set the paper size and margins to match your PDF settings, turn on "Background graphics", and turn off the browser's default headers and footers. This is the closest match to the final PDF. - DevTools media emulation: open DevTools, then the command menu, run Show Rendering, and set Emulate CSS media type to
print. This applies your print styles on screen so you can inspect elements and check computed break values, but it does not paginate, so it will not show where pages end. - Outline suspects temporarily. Add
outline: 1px solid redto elements with break rules and check print preview to see which ones land where you expect. - Check the computed display. In the Elements panel, confirm the element is
blockand its ancestors are not flex, grid, or scroll containers.
Applying page break rules with PDFGate
PDFGate's HTML to PDF API renders in Chromium, so everything above applies directly. You have two ways to deliver the rules.
Inject CSS into an existing URL. The css parameter adds a stylesheet to the page before rendering, so you can fix pagination on a page you do not want to change. When you convert a URL, PDFGate renders with screen media by default, so either put your rules in css, which applies regardless of media type, or set emulateMediaType to printto activate the page's own print styles.
cURL
1curl -H "Content-Type: application/json" \2 -H "Authorization: Bearer YOUR_API_KEY" \3 --request POST \4 --data '{5 "url": "https://example.com/report",6 "pageSizeType": "a4",7 "emulateMediaType": "print",8 "css": "h2, h3 { break-after: avoid; } .card, tr { break-inside: avoid; } thead { display: table-header-group; } .section { break-before: page; }"9 }' \10 https://api.pdfgate.com/generate/pdf \11 -o report.pdfSend the rules in your HTML. When you generate from a template, send the full document in the html parameter with the break rules in its stylesheet. Raw HTML renders with print media, so @media print blocks apply as written.
Node.js
1const fs = require("fs");23const html = `4<html>5 <head>6 <style>7 .invoice { break-after: page; }8 .invoice:last-child { break-after: auto; }9 thead { display: table-header-group; }10 tr, .totals { break-inside: avoid; }11 h2 { break-after: avoid; }12 </style>13 </head>14 <body>15 <section class="invoice">...</section>16 <section class="invoice">...</section>17 </body>18</html>`;1920async function generate() {21 const response = await fetch("https://api.pdfgate.com/generate/pdf", {22 method: "POST",23 headers: {24 "Content-Type": "application/json",25 "Authorization": "Bearer YOUR_API_KEY",26 },27 body: JSON.stringify({28 html,29 pageSizeType: "a4",30 margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },31 }),32 });3334 if (response.status === 201) {35 fs.writeFileSync("invoices.pdf", Buffer.from(await response.arrayBuffer()));36 } else {37 console.log(await response.json());38 }39}4041generate();Both requests return the PDF file directly. Iterate in Chrome print preview first, then move the same CSS into the request once the pages look right.
Frequently asked questions
How do I force a page break in HTML to 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 still works as an alias. The element must be a block-level box in normal flow, so the rule has no effect on inline, floated, or absolutely positioned elements.
How do I stop a table row or div from splitting across pages?
Apply break-inside: avoid (or the legacy page-break-inside: avoid) to the tr or div. Chromium then moves the whole element to the next page if it does not fit. If the element is taller than a full page, the rule cannot be satisfied and the content breaks anyway.
How do I repeat a table header on every PDF page?
Put the header cells in a thead element. Chromium repeats a thead (display: table-header-group) at the top of every page the table spans, and repeats a tfoot at the bottom. If you build the header with a plain tr or a div, it will not repeat.
Why is my page-break CSS being ignored?
The usual causes are a flex or grid parent, an inline or floated element, a scroll container with overflow: auto or a fixed height that clips the content, or an @media print rule that never applies because the page renders with screen media. Switch wrappers to display: block for print and remove fixed heights and overflow rules.
Should I use break-before or page-break-before?
Use break-before, break-after, and break-inside in new code. They are the standard properties and work in both paged media and multi-column layouts. Chromium still maps the legacy page-break-* properties onto them, so existing templates keep working.
Does PDFGate respect CSS page break rules?
Yes. PDFGate renders HTML in Chromium, so break-before, break-after, break-inside, orphans, widows, and repeating table headers behave as they do in Chrome print preview. You can include the rules in your HTML or inject them into any URL with the css parameter.
Learn more about rendering options on our HTML to PDF API page.