You added page-break-before: always and nothing happened. Or the PDF is a single page with half the content gone. Chrome (and so Puppeteer, Playwright and most HTML to PDF APIs) supports the CSS break properties well now, but it silently ignores them in a handful of situations. We printed 33 test layouts with Chrome to find out exactly which.
Each layout was printed to A4 with Chrome's page.pdf(), then every page was read back with pdftotext to see where each element landed.
| Layout | Result |
|---|---|
| Cards with no break rules 12 cards, each about 70mm tall | 2 of 12 cut in half |
break-inside: avoid on the cardsalso inside display: flex, flex-wrap and display: grid parents | 0 cut, works |
break-before: page on a block inside a flex or grid parent | works |
break-before: page on a <span>, an inline-block or a <br> | ignored |
break-before: page on a floated or position: absolute element | ignored |
App shell: html, body { height: 100% } and a scrolling .app { overflow: auto }same with height: 100vh or body { position: fixed } | 1 page, 8 of 12 cards missing |
<h2> that lands at the very bottom of a page | heading alone on page 1, its text on page 2 |
Same heading with break-after: avoid | moved to page 2 with its text |
60-row table, header in <thead> | header repeated on all 4 pages, 2 rows cut in half |
Same table with tr { break-inside: avoid } | 0 rows cut |
Header row inside <tbody> instead of <thead> | header on page 1 only |
Tested with Chromium 156 through our renderer (Playwright). The legacy page-break-before, page-break-after and page-break-inside properties behave the same: Chrome maps them to the break-* ones.
Content missing at the end. Single-page apps often make html and body 100% high and scroll inside a wrapper. Print keeps that box one viewport tall and clips the rest, so you get one page and no error. This is the usual cause of "Puppeteer PDF only prints the first page". Reset the heights and overflow in a print stylesheet (rule 1 below).
Breaks that do nothing. A forced break only applies to block-level elements in the normal flow. On an inline element, a float or an absolutely positioned box Chrome drops it without a warning. Put the break on a block wrapper, or give the element display: block and remove the float or positioning in print.
Adjust the selectors (#root, .app, .card) to your markup. With this added, the broken test page below printed 3 pages with every element where it should be, instead of 1 page.
@media print {
/* 1. Let the page grow: app shells that scroll inside a 100%-high box print one page. */
html, body, #root, .app {
height: auto !important;
overflow: visible !important;
position: static !important;
}
/* 2. Forced breaks only work on block-level, in-flow elements. */
.page-break { display: block; break-before: page; }
/* 3. Keep things whole. */
.card, figure, pre, tr, img { break-inside: avoid; }
/* 4. No heading stranded at the bottom of a page. */
h1, h2, h3 { break-after: avoid; }
/* 5. Repeat table headers on every page. */
thead { display: table-header-group; }
}
Use <div class="page-break"></div> wherever a new page should start; an empty block with a forced break works fine. break-after: avoid on headings is respected by current Chrome; older versions ignored it, which is why many answers online say it doesn't work.
This script loads a page in print mode and lists every forced break Chrome will ignore and every box that clips its content. Run it in CI before you ship a template: npm i puppeteer, save as check-breaks.mjs, then node check-breaks.mjs report.html. It exits with 1 when it finds something.
import puppeteer from 'puppeteer';
import { pathToFileURL } from 'node:url';
// Usage: node check-breaks.mjs page.html (or a URL)
const arg = process.argv[2] ?? 'page.html';
const target = arg.startsWith('http') ? arg : pathToFileURL(arg).href;
const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); // --no-sandbox: Ubuntu 23.10+ / Docker
const page = await browser.newPage();
await page.goto(target, { waitUntil: 'networkidle0' });
await page.emulateMediaType('print'); // check the layout that page.pdf() will print
const problems = await page.evaluate(() => {
const out = [];
const name = el => el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') +
[...el.classList].map(c => '.' + c).join('');
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
const forced = ['page', 'left', 'right'].some(v => s.breakBefore === v || s.breakAfter === v);
if (forced && s.display.startsWith('inline')) out.push(`${name(el)}: page break ignored (display: ${s.display})`);
else if (forced && s.float !== 'none') out.push(`${name(el)}: page break ignored (float: ${s.float})`);
else if (forced && ['absolute', 'fixed'].includes(s.position)) out.push(`${name(el)}: page break ignored (position: ${s.position})`);
const clipped = el.scrollHeight - el.clientHeight;
if (['auto', 'scroll', 'hidden'].includes(s.overflowY) && el.clientHeight > 0 && clipped > 1)
out.push(`${name(el)}: ${clipped}px cut off by overflow-y: ${s.overflowY} (only the visible part prints)`);
}
return out;
});
console.log(problems.length ? problems.join('\n') : 'No page-break problems found');
await browser.close();
process.exitCode = problems.length ? 1 : 0;
On a test page with all three mistakes it prints:
div.app: 846px cut off by overflow-y: auto (only the visible part prints)
span.chapter: page break ignored (display: inline)
div.side: page break ignored (float: right)
After adding the print stylesheet it prints No page-break problems found. Both runs were done with Puppeteer and Chrome for Testing 155.
/v1/pdf prints with Chrome in print media, so all of the above applies unchanged: put the print CSS in your HTML. This sends a local file (needs jq):
curl -s -X POST https://shotmint.creasework.com/v1/pdf \
-H "X-API-Key: $SHOTMINT_KEY" -H "Content-Type: application/json" \
-d "$(jq -n --rawfile html report.html '{html: $html, format: "A4", margin: "15mm"}')" \
-o report.pdf
Run against the live API with the fixed test page: a 3-page A4 PDF, 1 credit. Add "screen_media": true to print with your screen styles instead, and see the page numbers guide for headers and footers.
height: 100% or 100vh plus overflow wrappers around the content in print.<br>, floats or absolute positioning).break-inside: avoid on cards, figures, code blocks and table rows; it works inside flex and grid now.break-after: avoid on headings.<thead> so they repeat.pdftotext -f N -l N out.pdf -, not by eye.Related: HTML to PDF invoices, Tailwind print classes, lazy-loaded images in PDFs, HTML to PDF in Node.js.
One PDF = one credit, however many pages it has. €5 buys 2,000 renders, credits never expire and there's no subscription.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).
All guides: guides · integrations · comparisons