CSS print styles are CSS rules that only apply when a page is printed or exported to PDF. They control page margins, page breaks, typography, and element visibility, and they are the foundation of every HTML-to-PDF pipeline. Whether you use Playwright, Puppeteer, or an API like PDF4.dev, the browser applies @media print styles before generating the PDF.
This guide covers every print CSS property with interactive demos you can manipulate to see the effect in real time.
What does @media print do?
@media print is a CSS media query that targets print output. Styles inside this block only apply when the document is printed or rendered to PDF. The browser ignores them on screen.
The most common use is hiding non-essential UI elements: navigation bars, sidebars, cookie banners, and footers. Toggle between screen and print below to see what changes.
Three patterns appear in nearly every print stylesheet:
- Hide chrome: navigation, sidebars, ads, cookie banners get
display: none - Simplify colors: backgrounds become white, text becomes black, links lose their color
- Expand links:
a::after { content: " (" attr(href) ")"; }prints the URL next to each link, so readers can follow references on paper
@media print {
nav, .sidebar, footer, .cookie-banner {
display: none;
}
body {
background: white;
color: black;
font-size: 12pt;
line-height: 1.5;
}
a {
color: black;
text-decoration: underline;
}
a[href^="http"]::after {
content: " (" attr(href) ")";
font-size: 80%;
color: #666;
}
}You can test print styles without printing. In Chrome DevTools, open the Rendering panel and set "Emulate CSS media type" to "print". Firefox has a print toggle in the Style Editor.
How to set PDF page margins with CSS
The @page at-rule defines properties that apply to printed pages. Its most important property is margin, which sets the non-printable area around the content.
Use the sliders below to see how margin values affect the printable area.
Margin best practices for PDF
| Scenario | Recommended margins | Why |
|---|---|---|
| Standard document | 20mm 15mm | Comfortable reading, safe for all printers |
| Full-bleed design | 0 | Edge-to-edge content (posters, certificates) |
| Bound document | 20mm 10mm 20mm 25mm | Extra left margin for binding |
| Legal document | 25mm 20mm | Space for signatures and stamps |
Use absolute units (mm, cm, in) for print margins, not relative units like em or %. Absolute units produce consistent results across different PDF engines and printers. The CSS @page specification supports all absolute CSS length units.
/* Named page types with different margins */
@page {
margin: 20mm 15mm;
}
@page :first {
margin-top: 40mm; /* extra space for letterhead on page 1 */
}
@page :left {
margin-left: 25mm; /* binding margin on left pages */
}
@page :right {
margin-right: 25mm; /* binding margin on right pages */
}How to set page size and orientation with CSS
The size property inside @page controls the dimensions and orientation of printed pages. It accepts standard page names (A4, letter), explicit dimensions, or the landscape/portrait keywords.
/* Standard page sizes */
@page { size: A4; } /* 210mm x 297mm */
@page { size: A4 landscape; } /* 297mm x 210mm */
@page { size: letter; } /* 8.5in x 11in */
/* Custom dimensions */
@page { size: 148mm 210mm; } /* A5 */
@page { size: 100mm 100mm; } /* Square label */| Size keyword | Dimensions | Use case |
|---|---|---|
A4 | 210 x 297mm | Standard international documents |
A4 landscape | 297 x 210mm | Wide tables, presentations |
letter | 8.5 x 11in | US standard documents |
A5 | 148 x 210mm | Booklets, flyers |
Custom WxH | Any dimensions | Labels, tickets, badges |
The size property is supported in Chromium-based browsers and Playwright. Firefox ignores it and uses the user's print dialog settings instead. When generating PDFs with Playwright or PDF4.dev, size in CSS is overridden by the API's format parameter if both are specified.
For PDF generation via API, prefer passing page size through the format parameter rather than CSS @page { size }. This keeps your HTML portable across different output formats.
How to control page breaks in CSS
Page breaks determine where content splits across pages. CSS provides three properties for this: break-before, break-after, and break-inside. These replace the older page-break-* properties, which are now legacy aliases.
Toggle between break rules below to see how content distributes across pages.
The three break properties
| Property | What it does | Common value |
|---|---|---|
break-before | Forces or prevents a break before an element | page, avoid |
break-after | Forces or prevents a break after an element | page, avoid |
break-inside | Prevents a break inside an element | avoid |
Practical page break patterns
/* Force each chapter to start on a new page */
h2 {
break-before: page;
}
/* Keep figures and tables together (never split across pages) */
figure, table, .card {
break-inside: avoid;
}
/* Prevent a heading from being the last thing on a page */
h2, h3, h4 {
break-after: avoid;
}
/* Cover page: force a break after it */
.cover-page {
break-after: page;
}The legacy page-break-before, page-break-after, and page-break-inside properties still work in all browsers. They are treated as aliases for the modern break-* properties. Use the modern syntax in new projects.
What break-inside: avoid actually means
break-inside: avoid tells the browser "do not split this element across a page boundary." This is essential for elements that lose meaning when broken: tables, charts, cards, address blocks, code snippets.
If the element is taller than a full page, the browser ignores the rule and breaks it anyway. There is no way to prevent this with CSS alone.
/* Protect all critical blocks from splitting */
.invoice-line-item,
.testimonial-card,
.code-block,
.signature-block {
break-inside: avoid;
}How do CSS widows and orphans work?
Widows and orphans are typographic terms borrowed from print design. An orphan is a line stranded at the bottom of a page, and a widow is a line stranded at the top of the next page. Both look awkward and reduce readability.
CSS lets you set minimum line counts on each side of a page break. Drag the sliders below to see how different values redistribute text.
The default value for both widows and orphans is 2 in most browsers. Setting them to 3 produces noticeably better typographic results in long documents like reports, contracts, and articles.
p {
orphans: 3; /* at least 3 lines before a page break */
widows: 3; /* at least 3 lines after a page break */
}
/* For critical blocks, be more aggressive */
.legal-clause {
orphans: 4;
widows: 4;
}Widows and orphans are hints, not hard rules. If the browser cannot satisfy the constraint without creating an empty page, it may ignore the value. Chromium-based engines (Chrome, Playwright, PDF4.dev) handle these properties reliably for body text.
How to repeat table headers across printed pages
When a table spans multiple pages, readers on page 2 lose context because column headers are only on page 1. The fix is semantic HTML: wrap headers in <thead>. Chromium-based browsers automatically repeat <thead> content on every printed page.
Toggle between <thead> and no <thead> below to see the difference.
| Product | Q1 | Q2 | Q3 |
|---|---|---|---|
| Cloud Hosting | $12,400 | $14,200 | $16,800 |
| API Gateway | $8,300 | $9,100 | $11,500 |
| Storage | $5,600 | $6,200 | $7,100 |
| CDN | $3,200 | $3,800 | $4,500 |
| Auth Service | $2,100 | $2,400 | $2,900 |
| Product | Q1 | Q2 | Q3 |
|---|---|---|---|
| Analytics | $1,800 | $2,100 | $2,600 |
| $1,400 | $1,600 | $1,900 | |
| Webhooks | $900 | $1,100 | $1,400 |
This behavior comes from the CSS 2.1 specification and is implemented in Chrome, Firefox, and all Chromium-based PDF engines including Playwright. The same technique works for <tfoot>, which repeats at the bottom of every page.
<table>
<thead>
<tr>
<th>Product</th>
<th>Q1</th>
<th>Q2</th>
<th>Q3</th>
</tr>
</thead>
<tbody>
<!-- data rows -->
</tbody>
<tfoot>
<tr>
<td colspan="4">Report generated by PDF4.dev</td>
</tr>
</tfoot>
</table>For extra safety, add display: table-header-group explicitly in your print stylesheet:
@media print {
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
}PDF4.dev uses this exact <thead>/<tfoot> pattern for its component system. When you attach a header or footer component to a template, it gets rendered inside <thead> and <tfoot> so it repeats on every page automatically.
How does print-color-adjust work?
Browsers strip background colors and images in print mode by default to save ink. The print-color-adjust property overrides this behavior. Set it to exact to force the browser to render backgrounds, gradients, and images as they appear on screen.
@media print {
body {
-webkit-print-color-adjust: exact; /* Safari, older Chrome */
print-color-adjust: exact; /* standard */
}
}| Value | Behavior | When to use |
|---|---|---|
economy (default) | Browser may strip backgrounds to save ink | Text-heavy documents, contracts |
exact | All backgrounds, gradients, and images preserved | Invoices, certificates, branded reports |
The -webkit- prefix is still required for Safari. Chromium dropped the prefix in version 98, but including both ensures compatibility. When generating PDFs with Playwright, pass printBackground: true in the API call to achieve the same result.
Common CSS print pitfalls
Using viewport units for layout
vw and vh refer to the screen viewport, not the printed page. A width: 100vw element may overflow or underflow the page. Use %, mm, or cm for print layouts instead.
Not testing multi-page output
A stylesheet that looks fine on one page may break on page 2. Always test with enough content to trigger at least 3 pages. Apply break-inside: avoid on cards, figures, and tables to catch splitting issues early.
Forgetting @page interacts with API margins
When using a PDF generation API that accepts margin parameters (Playwright, PDF4.dev), the API margins override @page { margin }. If you set margins in both places, the API wins. Pick one source of truth: either CSS @page or the API parameter.
Browser and PDF engine compatibility
Not every CSS print property works everywhere. This table shows support across the engines that matter for PDF generation.
| Property | Chrome / Playwright | Firefox | Safari | wkhtmltopdf |
|---|---|---|---|---|
@media print | Yes | Yes | Yes | Yes |
@page { margin } | Yes | Yes | Yes | Yes |
@page { size } | Yes | No (ignored) | No | Partial |
break-before/after | Yes | Yes | Yes | Legacy only |
break-inside: avoid | Yes | Yes | Partial | Yes |
orphans / widows | Yes | Yes | No | No |
print-color-adjust | Yes | Yes | Prefix only | N/A |
<thead> repetition | Yes | Yes | Yes | Yes |
Chromium-based engines (Chrome, Playwright, PDF4.dev) have the most complete support. If you target Chromium for PDF generation, every property in this guide works reliably.
From CSS to production PDFs
CSS print styles work great for one-off printing from a browser. For automated PDF generation at scale, you need a rendering engine that applies these styles programmatically.
PDF4.dev uses Playwright (headless Chromium) under the hood. Every CSS property covered in this guide, from @page margins to break-inside: avoid to <thead> repetition, works exactly the same in generated PDFs as it does in Chrome's print preview.
curl -X POST https://pdf4.dev/api/v1/render \
-H "Authorization: Bearer p4_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"html": "<style>@page { margin: 20mm } h2 { break-before: page } thead { display: table-header-group }</style><h1>Report</h1>...",
"format": { "preset": "a4" }
}'The difference: with PDF4.dev, you skip browser management, Docker images, cold starts, and concurrency issues. One API call, one PDF. The CSS is the same either way.
HTML to PDFTry it freeFree tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.