Menu
Sign up

Print CSS for PDFs: @page, page breaks, headers and colours, tested in Chrome

Print CSS that works in Chrome's PDF output: @page size and margins, page numbers in margin boxes, breaks, repeated table headers and backgrounds, tested.

Published November 8, 2026 · 4 min read

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: page starts each section on a new page: our table section began on page 4.
  • break-inside: avoid keeps a block together: none of the 12 cards was split; the cards per page came out 4, 5 and 3.
  • break-after: avoid keeps a heading with what follows it, so a heading never ends a page alone.
  • orphans and widows set 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.

@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, or print-color-adjust: exact on what must keep its colour.
  • break-inside: avoid on cards, figures and table rows; break-after: avoid on headings.
  • Fonts loaded before printing: wait for document.fonts.ready after goto.
  • Test the PDF, not the screen: print preview in Chrome (Ctrl+P) uses the same engine and the same rules.

FAQ

How do I add page numbers to a PDF with CSS?

Use a page margin box: @page { @bottom-right { content: "Page " counter(page) " of " counter(pages); } }. Chrome supports margin boxes from version 131; in our test with Chrome 143 every page of an 8-page PDF printed "Page N of 8". The page needs a margin for the box to sit in.

Why are background colours missing from my PDF?

Chrome leaves backgrounds out of print by default. Either set printBackground: true in page.pdf(), which printed every background in our test, or mark the elements that must keep their colour with print-color-adjust: exact, which kept a blue band even with printBackground off.

Why does my PDF ignore @page size: A4?

Puppeteer's page.pdf() uses its own paper format (Letter by default) unless you pass preferCSSPageSize: true. In our test the same document came out 612×792 pt (Letter) without it and 595×842 pt (A4) with it.

How do I stop a table row or a card from being split across pages?

break-inside: avoid on the element. In our test none of 12 cards with that rule was split, and a 120-row table repeated its header on all five pages it covered (thead is a table-header-group by default; the rule only makes it explicit).