Print CSS is the part of CSS that decides how a page looks on paper or in a PDF: page size, margins, where pages break, what repeats on every page. Browsers have improved a lot here — Chrome 131 added page margin boxes, the feature that was missing for page numbers — but each rule still has conditions. This page lists the rules that matter for generating PDFs, each tested in Chrome's print engine.
How we tested. On 2026-11-08 we printed one test document — a coloured band, 12 cards, a 120-row table and a link — with Puppeteer 25.7's page.pdf() in Chrome 143.0.7499, three times: default options, with preferCSSPageSize: true, and with printBackground: true added. We read the PDFs back with PyMuPDF 1.28: page sizes, the text on each page, and the colour of pixels inside the backgrounds.
Page size and margins: @page
@page { size: A4; margin: 20mm 18mm 22mm; }
size takes keywords (A4, A5, letter, legal, plus landscape or portrait) or lengths (210mm 297mm). The catch in Puppeteer: page.pdf() has its own format, Letter by default, and it wins unless you pass preferCSSPageSize: true.
page.pdf() options |
Page size of the same document |
|---|---|
| none | 612 × 792 pt (US Letter) |
preferCSSPageSize: true |
595 × 842 pt (A4, as the CSS says) |
Set the size in one place. If you pass format: 'A4' in code, leave size out of the CSS, or the two drift apart.
Page numbers and running headers: margin boxes
@page {
@top-center { content: "Quarterly report"; font: 9pt sans-serif; color: #666; }
@bottom-right { content: "Page " counter(page) " of " counter(pages); font: 9pt sans-serif; }
}
@page :first { @top-center { content: none; } }
Chrome added page margin boxes in Chrome 131. In our PDF every page printed "Page N of 8" at the bottom right, and pages 2–8 had "Quarterly report" at the top while page 1, thanks to :first, did not. The boxes sit in the page margin, so they need one: with margin: 0 there is nowhere to draw them. There are sixteen of them (@top-left, @top-center, @bottom-right and so on), each a small box you can style with fonts and colours.
Before Chrome 131 the usual workaround was Puppeteer's headerTemplate/footerTemplate with displayHeaderFooter: true. It still works, but those templates are separate snippets styled inline, not part of your page's stylesheet; margin boxes are.
Where pages break
h2 { break-before: page; break-after: avoid; }
.card { break-inside: avoid; }
p { orphans: 3; widows: 3; }
break-before: pagestarts each section on a new page: our table section began on page 4.break-inside: avoidkeeps a block together: none of the 12 cards was split; the cards per page came out 4, 5 and 3.break-after: avoidkeeps a heading with what follows it, so a heading never ends a page alone.orphansandwidowsset the minimum lines of a paragraph left at the bottom or carried to the top of a page.
The older page-break-before/page-break-inside names still work as aliases.
Tables across pages
thead { display: table-header-group; }
tr { break-inside: avoid; }
Our 120-row table ran over five pages and the header row printed at the top of all five. table-header-group is the default display of <thead>, so this works without the rule; write it down when a reset stylesheet might have changed it. tfoot repeats the same way as table-footer-group. break-inside: avoid on rows keeps a tall row from being split between pages.
Backgrounds and colours
Browsers drop backgrounds when printing to save ink. Two ways to keep them, both tested:
Band with print-color-adjust: exact |
Band with economy |
Card background | |
|---|---|---|---|
printBackground off |
kept (28, 78, 216) | dropped (white) | dropped |
printBackground: true |
kept | kept | kept |
So printBackground: true in code prints everything; print-color-adjust: exact in CSS protects single elements even when the caller forgot it. Watch white text on a coloured band: if the band's background is dropped, the text is white on white. Add -webkit-print-color-adjust: exact too for older engines.
Print-only and screen-only content
@media print {
.screen-only { display: none; }
a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}
page.pdf() uses print media by default: our "screen only" banner was gone from the PDF and the link printed its address, "(https://example.com/method)". To print a page as it looks on screen instead, call page.emulateMediaType('screen') before page.pdf().
What our API does with print CSS
Our PDFs are not print-CSS documents, and it is better to know that before choosing it. A template rendered as "format": "pdf" is one page at the template's exact size, drawn with screen styles: the PDF is the same picture as the PNG with the text kept as vectors. A URL screenshot as PDF is Chrome printing the page with emulateMediaType('screen'), zero margins and pages the size of the viewport (all of them with full_page, otherwise the first). In both, @media print rules, @page sizes and margin boxes do not apply.
That suits one-page documents — certificates, tickets, labels, a one-page invoice (html-invoice-template). For paged documents with the rules above — reports, contracts, books — print them yourself with Puppeteer (page.pdf({ preferCSSPageSize: true, printBackground: true }), see Puppeteer PDF) or a dedicated engine such as WeasyPrint (HTML to PDF in Python compares them), or a multi-page PDF service.
Checklist
preferCSSPageSize: true, or the CSS page size is ignored in Puppeteer.- A page margin, or margin boxes have nowhere to go.
printBackground: true, orprint-color-adjust: exacton what must keep its colour.break-inside: avoidon cards, figures and table rows;break-after: avoidon headings.- Fonts loaded before printing: wait for
document.fonts.readyaftergoto. - Test the PDF, not the screen: print preview in Chrome (Ctrl+P) uses the same engine and the same rules.