CSS Paged Media and @page: Control PDF Pages with CSS

CSS paged media is the CSS for printed pages: @page size, margins, page breaks, named pages and running headers. What Chrome supports, tested.

By Simon Chiu · Updated

CSS paged media is the part of CSS that describes output split into pages: the @page rule sets page size, margins and margin-box content such as running headers and page numbers, and fragmentation properties such as break-before decide where pages end.

It is how you control a PDF from the HTML itself, instead of from a pile of options in your PDF code. This entry is part of the screenshot glossary.

What it means in plain terms

A screen is one long scrolling box. Paper is a series of fixed boxes. CSS paged media gives you rules for those boxes:

Feature CSS What it does
Page size @page { size: A5; } or size: 148mm 210mm Paper size and orientation
Margins @page { margin: 20mm 15mm; } Space around the content area
Margin boxes @page { @top-center { content: "Report"; } } Running headers and footers
Page counters counter(page), counter(pages) "Page 2 of 4"
First/left/right pages @page :first { ... } A cover with no header
Named pages @page wide { size: A5 landscape; } + page: wide One landscape page for a wide table
Breaks break-before: page, break-inside: avoid Start sections on a new page, keep rows whole

It is a separate job from a print stylesheet: @media print styles the elements, @page styles the pages they flow into.

A tested example

This is the stylesheet we printed:

<style>
  @page {
    size: A5;                      /* 148 x 210 mm */
    margin: 20mm 15mm;
    @top-center { content: "Quarterly report"; font: 9pt sans-serif; }
    @bottom-right { content: "Page " counter(page) " of " counter(pages); font: 9pt sans-serif; }
  }
  @page :first { @top-center { content: none; } }  /* no running header on the cover */
  @page wide { size: A5 landscape; }
  h2 { break-before: page; }
  table.data { page: wide; }      /* named page: this table gets a landscape page */
  tr { break-inside: avoid; }
</style>

The body has a cover, two <h2> sections and one wide table. The result, rendered from the PDF:

PDF page 1, A5 portrait: the heading Cover and one line of text, no running header at the top, and Page 1 of 4 at the bottom right PDF page 2, A5 portrait: the running header Quarterly report centered at the top, the heading Summary, and Page 2 of 4 at the bottom right PDF page 3, A5 landscape: the running header Quarterly report, a one-row table with Region and Q1 to Q4, and Page 3 of 4
Pages 1 to 3 of the 4-page PDF, rasterized at 60 dpi with pdftoppm. Printed with Puppeteer 24.43.1 and chrome-headless-shell 148 using preferCSSPageSize: true, 2026-10-15. Local renders, not API renders.

pdfinfo confirmed the sizes: pages 1, 2 and 4 are 420 × 595 pt (A5 portrait) and page 3 is 595 × 420 pt (A5 landscape, from the named page). The cover has no running header, from @page :first.

How it works technically

flowchart LR
  H[HTML + CSS] --> L["Layout for print media<br/>(@media print applies)"]
  L --> F["Fragmentation into page boxes<br/>break-before, break-inside, orphans"]
  F --> P["@page rules per page<br/>size, margin, :first, named pages"]
  P --> M["Margin boxes<br/>@top-center, counter(page)"]
  M --> O[PDF pages]
  X["PDF tool options<br/>format, margin, landscape"] -.->|override unless preferCSSPageSize| P

The dotted arrow is the trap. A PDF API call usually sends its own paper format. When we printed the same HTML with Puppeteer's format: 'letter', every page came out at Letter size (612 × 792 pt), with page 3 still turned to landscape. The named page kept its orientation, but the CSS size lost to the API option.

The setting in Puppeteer, Playwright and RenderScreenshot

Puppeteer:

import puppeteer from 'puppeteer';
import { readFile } from 'node:fs/promises';

const html = await readFile('paged.html', 'utf8');
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setContent(html);
  await page.pdf({ path: 'css-size.pdf', preferCSSPageSize: true }); // @page decides the size
  await page.pdf({ path: 'letter.pdf', format: 'letter' });          // format wins
} finally {
  await browser.close();
}

Playwright (PDF works in Chromium only):

import { chromium } from 'playwright';
import { readFile } from 'node:fs/promises';

const browser = await chromium.launch();
try {
  const page = await browser.newPage();
  await page.setContent(await readFile('paged.html', 'utf8'));
  await page.pdf({ path: 'css-size-pw.pdf', preferCSSPageSize: true });
} finally {
  await browser.close();
}

Playwright's PDF had the same four pages, sizes and margin text.

RenderScreenshot: send the HTML (or a URL), ask for a PDF and set pdf.prefer_css_page_size:

curl -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html paged.html '{html: $html, output: {format: "pdf"}, pdf: {prefer_css_page_size: true}}')" \
  --output report.pdf

The GET form is pdf_prefer_css_page_size=true. The PDF options also cover pdf.header and pdf.footer templates, the alternative to margin boxes when you would rather keep headers out of your CSS. For print-styled images instead of PDFs, see print media screenshots.

Tested on 2026-10-15 on macOS 27 (arm64) with Node 22.18.0, [email protected] (Chrome 148.0.7778.97, headless: 'shell') and [email protected] (Chromium 156.0.8078.4). PDFs checked with pdfinfo and pdftotext (poppler). The API request was not run for this page.

Common mistakes

  • Forgetting preferCSSPageSize. Your @page size is ignored and the paper format from code wins.
  • Margins in two places. CSS @page margins and the PDF tool's margin option both apply in different ways. Choose one source of truth.
  • Using page-break-* everywhere. The old properties still work as aliases, but break-before, break-after and break-inside are the current names.
  • Backgrounds that disappear. Printing drops background colors unless the tool prints backgrounds (printBackground in Puppeteer, pdf.print_background in RenderScreenshot) or the CSS sets print-color-adjust: exact.

To turn paged HTML into PDFs without running Chrome, get a free API key.

Frequently asked questions

What is the difference between @media print and @page?
@media print changes how elements look when printed, for example hiding the navigation. @page describes the page boxes themselves: paper size, margins and the content of the margins. A good print setup uses both.
Does Chrome support @page margin boxes like @top-center?
Yes, in current versions. In our test with Chrome 148 and Chromium 156, @top-center and @bottom-right with counter(page) and counter(pages) printed a running title and Page 2 of 4 in the PDF.
Why does my @page size get ignored?
Most PDF tools pass a paper format, which wins unless you tell them to prefer the CSS size: preferCSSPageSize in Puppeteer and Playwright, pdf.prefer_css_page_size in RenderScreenshot.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.