# 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 2026-11-08 · https://shareglint.com/en/guides/print-css-for-pdf

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`

```css
@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

```css
@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](https://developer.chrome.com/blog/print-margins). 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

```css
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

```css
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`](https://developer.mozilla.org/en-US/docs/Web/CSS/print-color-adjust) 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

```css
@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](https://shareglint.com/en/guides/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](https://shareglint.com/en/guides/puppeteer-pdf)) or a dedicated engine such as WeasyPrint ([HTML to PDF in Python](https://shareglint.com/en/guides/html-to-pdf-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).
