Controlling page layout, breaks, typography, and rendering when targeting print media or generating PDFs with headless browsers.
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.
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 */
}
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;
}
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;
}
}
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.
| Property | Values | Effect |
|---|---|---|
| break-before | auto, page, avoid, left, right, column | Break before this element |
| break-after | auto, page, avoid, left, right, column | Break after this element |
| break-inside | auto, avoid, avoid-page, avoid-column | Prevent 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;
}
}
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.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.
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:
| Value | Behavior |
|---|---|
| economy | Browser may optimize for ink usage (default). May remove backgrounds. |
| exact | Render colors exactly as specified. Background images and colors are preserved. |
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.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;
}
}
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;
}
}
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);
}
Hyperlinks are meaningless on paper. Two common approaches: hide the href entirely, or append it after the link text using ::after.
css@media print {
/* Append full URL after link text */
a[href]::after {
content: " (" attr(href) ")";
font-size: 0.85em;
color: #555;
word-break: break-all;
}
/* Skip internal anchors and JS links */
a[href^="#"]::after,
a[href^="javascript:"]::after {
content: "";
}
}
word-break: break-all on the ::after element is essential. Without it, a URL longer than the column width will overflow the page margin.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.
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();
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();
| Option | Type | Notes |
|---|---|---|
| printBackground | boolean | Must be true for colors to render |
| format | string | A0–A6, Letter, Legal, Tabloid, Ledger |
| width / height | string | Override format with explicit dimensions: "210mm" |
| preferCSSPageSize | boolean | If true, honor @page { size } from CSS instead of the API option |
| displayHeaderFooter | boolean | Puppeteer's own header/footer system (separate from CSS margin boxes) |
| waitUntil (goto) | string | Use networkidle0 to ensure fonts and images load before capture |
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.
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.
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.
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 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.
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>.
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.
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.