CSS for Print & PDF Generation

Controlling page layout, breaks, typography, and rendering when targeting print media or generating PDFs with headless browsers.

1. @media print

All print-specific rules go inside @media print { }. These rules are ignored on screen. Use a separate stylesheet loaded with media="print", or embed the block inline. Both approaches are valid; the inline block is easier to maintain alongside the screen styles it overrides.

css/* Inline approach */
@media print {
  .sidebar, nav, .ads, button {
    display: none;
  }

  body {
    font-size: 12pt;
    line-height: 1.5;
    color: #000;
    background: transparent;
  }

  a {
    color: inherit;
    text-decoration: none;
  }
}
html<!-- External stylesheet approach -->
<link rel="stylesheet" href="print.css" media="print">

Use pt units for print typography. Pixels are screen-relative; points are physical (1pt = 1/72 inch). For layout dimensions, use mm, cm, or in.

2. @page rule

The @page at-rule sets page-level properties: margins, size, and orientation. It applies globally, or you can target specific pages with pseudo-classes.

css@page {
  size: A4 portrait;     /* or: letter, legal, A3, 210mm 297mm */
  margin: 20mm 15mm 25mm 15mm;
}

@page :first {
  margin-top: 40mm;       /* extra space on page 1 */
}

@page :left {
  margin-left: 25mm;      /* wider inner margin on left (even) pages */
  margin-right: 15mm;
}

@page :right {
  margin-left: 15mm;
  margin-right: 25mm;
}

@page :blank {
  margin: 20mm;            /* intentionally blank pages */
}

Named pages

You can define named page contexts and assign them to elements. This lets different sections use different page sizes or orientations within the same document.

css@page landscape-page {
  size: A4 landscape;
}

.wide-table-section {
  page: landscape-page;
}
Browser support: Named pages are well-supported in Chrome/Edge 85+. Firefox support as of 2024 is partial. Headless Chrome (Puppeteer/Playwright) supports named pages fully.

Page margin boxes

CSS defines 16 margin box regions around each page for headers and footers. Content inside them repeats on every page.

css@page {
  margin-bottom: 30mm;

  @bottom-center {
    content: counter(page);
    font-size: 9pt;
    color: #666;
  }

  @top-right {
    content: string(chapter-title);
    font-size: 8pt;
  }
}
Note: Margin boxes are part of CSS Paged Media Level 3. They work in headless Chrome and in dedicated print engines (Prince, WeasyPrint, PDFreactor). Margin boxes do not render in regular browser print dialogs.

3. Page breaks

Two property families control page breaks. The older page-break-* properties are widely supported. The newer break-* properties (part of CSS Fragmentation) are the standard — use both for compatibility.

PropertyValuesEffect
break-beforeauto, page, avoid, left, right, columnBreak before this element
break-afterauto, page, avoid, left, right, columnBreak after this element
break-insideauto, avoid, avoid-page, avoid-columnPrevent break inside element
css@media print {
  /* Force each chapter onto a new page */
  h1, .chapter {
    break-before: page;
    page-break-before: always; /* legacy fallback */
  }

  /* Keep headings with the content that follows */
  h2, h3, h4 {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* Never split a figure or table across pages */
  figure, table, blockquote, .card {
    break-inside: avoid;
    page-break-inside: avoid;
  }
}
Gotcha: break-inside: avoid on a container that is taller than a full page will be ignored — the browser has no choice but to split it. For very long tables, this is unavoidable without restructuring the HTML.

4. Orphans & widows

Orphans are lone lines at the bottom of a page; widows are lone lines at the top. Both are typographically undesirable. CSS controls the minimum number of lines that must appear on either side of a page break.

css@media print {
  p {
    orphans: 3; /* min lines at bottom of page */
    widows:  3; /* min lines at top of next page */
  }
}

The default browser value for both is 2. Setting 3 is a reasonable typographic baseline for body text. These properties only affect paragraphs that can actually be split — they have no effect when combined with break-inside: avoid.

5. Color & backgrounds

By default, browsers strip background colors and images when printing to save ink. Override this behavior explicitly when backgrounds carry semantic meaning (e.g., status badges, chart fills).

css.badge--error {
  background-color: #dc2626;
  color: #fff;
  print-color-adjust: exact;        /* standard */
  -webkit-print-color-adjust: exact; /* Chrome/Safari */
}

The print-color-adjust property accepts two values:

ValueBehavior
economyBrowser may optimize for ink usage (default). May remove backgrounds.
exactRender colors exactly as specified. Background images and colors are preserved.
Performance tip: Apply print-color-adjust: exact only to specific elements, not globally. Applying it to the root element forces the renderer to produce every background pixel, which can significantly slow PDF generation for complex layouts.

Dark-mode print reset

If your page supports dark mode via prefers-color-scheme, explicitly reset to a white background for print so PDFs aren't generated in dark mode when the user's OS is in dark mode.

css@media print {
  :root {
    color-scheme: light;
    --bg: #fff;
    --text: #111;
  }
}

6. Page counters & running content

CSS counters combined with content in @page margin boxes produce automatic page numbers and chapter names without JavaScript. This requires a CSS Paged Media–capable engine.

css/* Page numbers in bottom-center */
@page {
  @bottom-center {
    content: counter(page) " / " counter(pages);
    font-size: 9pt;
    font-family: sans-serif;
    color: #666;
  }
}

/* Running chapter title via string-set */
h1 {
  string-set: chapter-title content();
}

@page {
  @top-left {
    content: string(chapter-title);
    font-size: 8pt;
    color: #888;
  }
}

CSS counter fallback for browser print

When targeting regular browser print (not a paged media engine), approximate page numbers via ::after pseudo-elements. This is not true page numbering — it counts elements, not physical pages — but is a workable fallback for simple documents.

css.chapter {
  counter-increment: section;
}

.chapter::before {
  content: "Section " counter(section);
}

8. Headless browser PDF generation

Headless Chrome (via Puppeteer or Playwright) renders CSS Paged Media properties more completely than the browser's native print dialog. It supports @page size, named pages, and margin boxes.

Puppeteer

jsimport puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page    = await browser.newPage();

await page.goto('https://example.com/report', {
  waitUntil: 'networkidle0'  // wait for all assets
});

await page.pdf({
  path:              'report.pdf',
  format:            'A4',
  printBackground:   true,  // honor print-color-adjust: exact
  displayHeaderFooter: false,
  margin: {
    top:    '20mm',
    bottom: '20mm',
    left:   '15mm',
    right:  '15mm'
  }
});

await browser.close();

Playwright

jsimport { chromium } from 'playwright';

const browser = await chromium.launch();
const page    = await browser.newPage();

await page.goto('https://example.com/report');
await page.waitForLoadState('networkidle');

await page.pdf({
  path:            'report.pdf',
  format:          'A4',
  printBackground: true,
  margin:          { top: '20mm', bottom: '20mm' }
});

await browser.close();

Key options

OptionTypeNotes
printBackgroundbooleanMust be true for colors to render
formatstringA0–A6, Letter, Legal, Tabloid, Ledger
width / heightstringOverride format with explicit dimensions: "210mm"
preferCSSPageSizebooleanIf true, honor @page { size } from CSS instead of the API option
displayHeaderFooterbooleanPuppeteer's own header/footer system (separate from CSS margin boxes)
waitUntil (goto)stringUse networkidle0 to ensure fonts and images load before capture

Server-side alternatives

WeasyPrint (Python) implements CSS Paged Media Level 3 more completely than Chrome, including string-set, footnotes, and complex margin box content. Use it when the document is document-like rather than app-like. Prince is the commercial gold standard but is proprietary. PDFreactor is another commercial option with excellent standards support.

9. Common gotchas

Flexbox and grid fragmentation

Flex and grid containers often do not fragment correctly across pages. Browsers treat them as monolithic when they contain break-inside: avoid, but even without it, fragmentation behavior is inconsistent. For complex multi-page layouts, block-level formatting (standard div stacking) is more reliable than flex columns.

Font loading in headless

Headless Chrome loads web fonts over the network. If waitUntil: 'networkidle0' is not set, the PDF may render with system fallback fonts instead of the intended typeface. For offline generation, embed fonts via @font-face with base64 data URIs, or use locally installed fonts.

CSS custom properties in @page

Custom properties (CSS variables) do not work inside @page rules. The @page context is not part of the DOM and has no element to inherit variables from. Use static values instead.

css/* ✗ Does not work */
@page {
  margin: var(--page-margin);
}

/* ✓ Use static values */
@page {
  margin: 20mm 15mm;
}

Images and SVG scaling

Images in print should be sized with explicit max-width: 100% and ideally use physical units. SVGs that rely on viewBox without width/height attributes may render at unexpected sizes in print contexts. Set both attributes, or use CSS to constrain them explicitly.

Table header repetition

Use <thead> for table header rows. In print, the browser automatically repeats <thead> at the top of each new page when a table breaks across pages. This does not work with rows inside <tbody>.

Chrome vs. print dialog margins

The browser's native print dialog adds its own margin around the page. These are distinct from, and additive with, @page margins. When generating PDFs via headless, the print dialog is bypassed entirely, so @page margins are the sole source of truth. Test in both environments separately.

Transition and animation

CSS transitions and animations are suspended during print rendering. There is no concept of time in a printed page. Any layout that depends on animation state (e.g., a dropdown that is open only after a transition completes) must be explicitly reset to its final state in @media print.