Master CSS paged media for professional PDF generation.
Control page dimensions and margins:
@page {
size: A4; /* Paper size */
margin: 2cm; /* Content margins */
}
@page { size: A4; } /* 210mm × 297mm */
@page { size: A4 landscape; } /* 297mm × 210mm */
@page { size: Letter; } /* 8.5in × 11in */
@page { size: Letter landscape; }
@page { size: Legal; } /* 8.5in × 14in */
@page { size: A3; } /* 297mm × 420mm */
@page { size: A5; } /* 148mm × 210mm */
@page { size: 210mm 297mm; } /* Custom dimensions */
@page :first { margin-top: 5cm; } /* First page only */
@page :left { margin-left: 3cm; } /* Even pages (binding) */
@page :right { margin-right: 3cm; } /* Odd pages (binding) */
@page :blank { /* styles */ } /* Intentionally blank pages */
@page cover { margin: 0; }
@page chapter { margin: 2.5cm; }
@page appendix { size: A4 landscape; }
.cover { page: cover; }
.chapter { page: chapter; }
.appendix { page: appendix; }
Place content in page margins:
@page {
size: A4;
margin: 2.5cm 2cm;
@top-center {
content: "Document Title";
font-size: 10pt;
color: #666;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
}
@bottom-left {
content: "Confidential";
font-size: 9pt;
color: #999;
}
}
@top-left-corner @top-left @top-center @top-right @top-right-corner
@left-top @right-top
@left-middle [page content] @right-middle
@left-bottom @right-bottom
@bottom-left-corner @bottom-left @bottom-center @bottom-right @bottom-right-corner
@page {
@top-left {
content: url("https://example.com/logo.png");
height: 30px;
}
}
The content property in margin boxes only supports strings and url(). To place rich HTML (logos with text, styled layouts, multi-element content) in headers/footers, use position: running() with element():
/* 1. Remove element from flow and name it */
.running-header {
position: running(headerContent);
}
/* 2. Place it in a margin box */
@page {
margin-top: 3cm; /* Ensure enough space */
@top-center {
content: element(headerContent);
}
}
<!-- This element is pulled out of the flow and placed in the header -->
<div class="running-header">
<img src="https://example.com/logo.png" style="height: 20px; vertical-align: middle;" />
<span style="font-size: 10pt; color: #666; margin-left: 8pt;">Company Name</span>
</div>
<p>Document content starts here...</p>
Key behaviors:
Chapter-aware headers:
.chapter-header {
position: running(chapterHead);
}
@page {
@top-left {
content: element(chapterHead);
}
}
<section>
<div class="chapter-header">Chapter 1: Introduction</div>
<p>Content for chapter 1...</p>
</section>
<section class="chapter">
<!-- This replaces the running header from this page onward -->
<div class="chapter-header">Chapter 2: Analysis</div>
<p>Content for chapter 2...</p>
</section>
When to use running() vs string-set:
| Feature | string-set + string() |
position: running() + element() |
|---|---|---|
| Text only | Yes | Yes |
| Images/logos | No | Yes |
| Styled HTML | No | Yes |
| Multiple elements | No | Yes |
| Simpler syntax | Yes | No |
@page {
@bottom-center {
content: counter(page); /* Current page: 5 */
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages); /* Page 5 of 20 */
}
}
/* Reset counter for sections */
.chapter { counter-reset: page 1; }
/* Roman numerals for front matter */
@page frontmatter {
@bottom-center { content: counter(page, lower-roman); } /* i, ii, iii */
}
.frontmatter { page: frontmatter; }
/* Custom counters */
body { counter-reset: chapter; }
h1 { counter-increment: chapter; }
h1::before { content: "Chapter " counter(chapter) ": "; }
Always use both modern and legacy properties for compatibility:
/* Prevent breaks inside elements */
.no-break {
break-inside: avoid;
page-break-inside: avoid;
}
/* Force break before */
.chapter {
break-before: page;
page-break-before: always;
}
/* Force break after */
.page-end {
break-after: page;
page-break-after: always;
}
/* Keep heading with following content */
h1, h2, h3 {
break-after: avoid;
page-break-after: avoid;
}
/* Avoid breaks before specific elements */
.keep-with-previous {
break-before: avoid;
page-break-before: avoid;
}
| Value | Description |
|---|---|
auto |
Default, automatic breaks |
avoid |
Prevent break if possible |
always |
Always force break |
page |
Force page break |
left |
Break to next left (even) page |
right |
Break to next right (odd) page |
Page breaks only work on block-level elements:
@media print {
* { float: none !important; }
}
Control line distribution across pages:
p {
orphans: 3; /* Minimum lines at bottom of page */
widows: 3; /* Minimum lines at top of next page */
}
/* Strict: prevent any splitting */
.no-split {
orphans: 99;
widows: 99;
}
table {
width: 100%;
border-collapse: collapse;
}
/* Repeat header on each page */
thead { display: table-header-group; }
/* Repeat footer on each page */
tfoot { display: table-footer-group; }
/* Prevent row splitting */
tr {
break-inside: avoid;
page-break-inside: avoid;
}
/* Keep caption with table */
caption {
break-after: avoid;
page-break-after: avoid;
}
thead::after {
content: "(continued)";
display: none;
}
@media print {
thead::after { display: table-cell; }
}
WeasyPrint does not execute JavaScript. Chart libraries like Chart.js, D3.js, Plotly, Highcharts, or ApexCharts will not render. Charts must be prerendered before passing HTML to the API.
SVG is the best format for charts in PDFs — vector, scalable, and styled with CSS:
<div class="chart no-break">
<h3>Monthly Revenue</h3>
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- Bar chart -->
<rect x="20" y="120" width="40" height="80" fill="#3498db"/>
<rect x="80" y="80" width="40" height="120" fill="#3498db"/>
<rect x="140" y="40" width="40" height="160" fill="#3498db"/>
<rect x="200" y="60" width="40" height="140" fill="#3498db"/>
<!-- Labels -->
<text x="40" y="215" text-anchor="middle" font-size="10">Jan</text>
<text x="100" y="215" text-anchor="middle" font-size="10">Feb</text>
<text x="160" y="215" text-anchor="middle" font-size="10">Mar</text>
<text x="220" y="215" text-anchor="middle" font-size="10">Apr</text>
<!-- Axis -->
<line x1="10" y1="200" x2="260" y2="200" stroke="#333" stroke-width="1"/>
<line x1="10" y1="0" x2="10" y2="200" stroke="#333" stroke-width="1"/>
</svg>
</div>
Use base64-encoded chart images when SVG is not available:
<img src="data:image/png;base64,iVBORw0KGgo..."
alt="Sales Chart"
style="width: 100%; max-width: 600px;" />
Or reference a hosted image URL (provide base_url if using relative paths):
<img src="https://example.com/charts/revenue-q4.png"
alt="Revenue Chart"
style="width: 100%; max-width: 600px;" />
.chart {
break-inside: avoid;
page-break-inside: avoid;
margin: 1cm 0;
text-align: center;
}
.chart svg {
max-width: 100%;
height: auto;
}
.chart img {
max-width: 100%;
height: auto;
}
/* SVG text inherits nothing — set explicitly */
.chart svg text {
font-family: Arial, sans-serif;
font-size: 10pt;
fill: #333;
}
Since JavaScript won't execute, generate your chart output before sending HTML to the API:
| Approach | How |
|---|---|
| Server-side SVG | Generate SVG strings with a charting library on your server (e.g. D3 in Node.js, matplotlib in Python) and embed inline |
| Export from JS library | Use Chart.js .toBase64Image(), Plotly .toImage(), or D3 .node().outerHTML to export, then embed the result |
| Static image service | Use QuickChart.io or similar API to generate chart images from a URL |
| Python matplotlib/seaborn | Save as SVG with plt.savefig(buf, format='svg') and embed the SVG string |
| Plotly (Python, no JS needed) | Use plotly.io.to_image() or plotly.io.to_html(full_html=False, include_plotlyjs=False) to export static SVG/PNG — see example below |
If you can execute Python (e.g. via code interpreter, a script, or a server), Plotly is an excellent choice — it produces high-quality charts and exports static SVG without a browser:
import plotly.graph_objects as go
import plotly.io as pio
# Create chart
fig = go.Figure(data=[
go.Bar(x=["Jan", "Feb", "Mar", "Apr"], y=[120, 180, 240, 200],
marker_color="#3498db")
])
fig.update_layout(
title="Monthly Revenue",
width=600, height=350,
margin=dict(l=40, r=20, t=50, b=40),
font=dict(family="Arial, sans-serif", size=12)
)
# Export as SVG string — embed directly in HTML
svg_str = pio.to_image(fig, format="svg").decode("utf-8")
# Or export as base64 PNG
import base64
png_bytes = pio.to_image(fig, format="png", scale=2)
png_b64 = base64.b64encode(png_bytes).decode("utf-8")
Then embed in your HTML template:
<!-- Inline SVG (preferred) -->
<div class="chart no-break">
{svg_str}
</div>
<!-- Or as base64 image -->
<div class="chart no-break">
<img src="data:image/png;base64,{png_b64}" alt="Monthly Revenue" style="width:100%; max-width:600px;" />
</div>
Tip: Plotly requires the
kaleidopackage for static export:pip install plotly kaleido
<canvas> rendering — Canvas requires JavaScript; it will be blankfont-family — Use web-safe fonts; custom fonts must be embedded or available on the serverviewBox for proper scaling; avoid fixed width/height in pxAuto-generate a clickable PDF table of contents sidebar:
h1 { -pdfmcp-bookmark-level: 1; }
h2 { -pdfmcp-bookmark-level: 2; }
h3 { -pdfmcp-bookmark-level: 3; }
/* Custom bookmark label */
h1 { -pdfmcp-bookmark-label: content(text); }
/* Collapsed by default */
h2 { -pdfmcp-bookmark-state: closed; }
Set -pdfmcp-bookmark-level: none to exclude an element from the outline.
Auto-generate "See page X" references that update dynamically:
a.page-ref::after {
content: " (page " target-counter(attr(href), page) ")";
}
<p>Details in the <a class="page-ref" href="#appendix">Appendix</a>.</p>
<!-- Renders: "Details in the Appendix (page 12)." -->
<h2 id="appendix">Appendix</h2>
Move inline content to the bottom of the page as proper footnotes:
.footnote { float: footnote; }
::footnote-call { font-size: 0.7em; vertical-align: super; }
::footnote-marker::after { content: ". "; }
@page { @footnote { border-top: 0.5pt solid #ccc; padding-top: 5pt; } }
<p>This claim requires evidence<span class="footnote">Source: Annual Report 2024, p. 42</span> and further analysis.</p>
Improve justified text by enabling automatic hyphenation:
html { lang: "en"; } /* or set lang="en" on the <html> tag */
p {
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
.two-col {
columns: 2;
column-gap: 1.5em;
column-rule: 1px solid #ddd;
}
/* Headings span all columns */
.two-col h2 { column-span: all; }
/* Prevent breaks inside columns */
.two-col p { break-inside: avoid-column; }
Backgrounds may be stripped in print. Force them to render:
.colored-box {
background: #f0f0f0;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
@font-face {
font-family: 'CustomFont';
src: url('https://example.com/font.woff2') format('woff2');
}
/* Or Google Fonts via <link> in HTML */
body { font-family: 'CustomFont', Arial, sans-serif; }
Use absolute URLs or base64-encoded fonts for reliability. Google Fonts CDN links work if the server has internet access.
When a decorated element (borders, padding, backgrounds) splits across pages, clone repeats the decoration on each fragment:
.callout {
border: 2px solid #3498db;
padding: 1em;
background: #f0f8ff;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
Without clone, only the first fragment gets the top border/padding and the last gets the bottom.
Use leader() to create dot leaders (or other fill characters) between text and page numbers:
.toc a::after {
content: leader(".") target-counter(attr(href), page);
}
<ul class="toc">
<li><a href="#ch1">Introduction</a></li> <!-- Introduction .......... 1 -->
<li><a href="#ch2">Methods</a></li> <!-- Methods ............... 5 -->
<li><a href="#ch3">Results</a></li> <!-- Results .............. 12 -->
</ul>
Leader fill options: leader("."), leader(" "), leader("_"), or any single character.
Apply styles only when printing/generating PDFs:
/* Screen-only styles */
@media screen {
.print-only { display: none; }
}
/* Print-only styles */
@media print {
.no-print { display: none !important; }
body {
font-size: 12pt;
line-height: 1.5;
color: #000;
background: #fff;
}
a { color: #000; text-decoration: underline; }
/* Show URLs */
a[href]::after {
content: " (" attr(href) ")";
font-size: 9pt;
color: #666;
}
/* Reset problematic properties */
* {
float: none !important;
position: static !important;
overflow: visible !important;
}
}
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: A4;
margin: 2.5cm 2cm 3cm 2cm;
@top-center {
content: "Quarterly Report Q4 2024";
font-size: 10pt;
color: #666;
border-bottom: 0.5pt solid #ccc;
padding-bottom: 5pt;
}
@bottom-left {
content: "Confidential";
font-size: 9pt;
color: #999;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
}
}
@page :first {
margin-top: 4cm;
@top-center { content: none; }
}
@page chapter {
@top-left {
content: string(chapter-title);
}
}
body {
font-family: "Helvetica Neue", Arial, sans-serif;
font-size: 11pt;
line-height: 1.6;
color: #333;
}
h1 {
font-size: 24pt;
text-align: center;
margin-bottom: 2cm;
}
h2 {
font-size: 16pt;
color: #2c3e50;
border-bottom: 2pt solid #3498db;
padding-bottom: 5pt;
break-after: avoid;
page-break-after: avoid;
string-set: chapter-title content();
}
.chapter {
page: chapter;
break-before: page;
page-break-before: always;
}
p { orphans: 3; widows: 3; }
.no-break {
break-inside: avoid;
page-break-inside: avoid;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1cm 0;
}
thead { display: table-header-group; }
th { background: #f8f9fa; }
th, td { border: 1px solid #ddd; padding: 8pt; }
tr { break-inside: avoid; page-break-inside: avoid; }
@media print {
* { float: none !important; }
.no-print { display: none !important; }
}
</style>
</head>
<body>
<h1>Quarterly Report Q4 2024</h1>
<p class="no-print">This is a preview. Download the PDF for the full report.</p>
<section class="chapter">
<h2>Executive Summary</h2>
<div class="no-break">
<p>Key achievements this quarter:</p>
<ul>
<li>Revenue growth of 23%</li>
<li>Customer satisfaction at 94%</li>
<li>New market expansion completed</li>
</ul>
</div>
</section>
<section class="chapter">
<h2>Financial Overview</h2>
<table>
<thead>
<tr><th>Metric</th><th>Q3</th><th>Q4</th><th>Change</th></tr>
</thead>
<tbody>
<tr><td>Revenue</td><td>$1.2M</td><td>$1.5M</td><td>+23%</td></tr>
<tr><td>Expenses</td><td>$800K</td><td>$850K</td><td>+6%</td></tr>
<tr><td>Net Profit</td><td>$400K</td><td>$650K</td><td>+63%</td></tr>
</tbody>
</table>
</section>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: Letter;
margin: 1in 1.25in;
@bottom-center {
content: "- " counter(page) " -";
font-size: 10pt;
}
@bottom-left {
content: "AGREEMENT";
font-size: 8pt;
color: #666;
}
}
body {
font-family: "Times New Roman", Times, serif;
font-size: 12pt;
line-height: 2;
text-align: justify;
}
h1 { text-align: center; font-size: 14pt; text-transform: uppercase; }
.section { break-inside: avoid; page-break-inside: avoid; }
.signature-block { break-inside: avoid; page-break-inside: avoid; margin-top: 2cm; }
p { orphans: 4; widows: 4; }
</style>
</head>
<body>
<h1>Service Agreement</h1>
<div class="section">
<p><strong>1. PARTIES.</strong> This Agreement is entered into between
Company A ("Provider") and Company B ("Client").</p>
</div>
<div class="section">
<p><strong>2. SERVICES.</strong> Provider agrees to deliver the services
described in Exhibit A attached hereto.</p>
</div>
<div class="signature-block">
<p>IN WITNESS WHEREOF, the parties have executed this Agreement.</p>
<p style="margin-top: 1in;">_______________________________<br>
Provider Signature</p>
<p style="margin-top: 0.5in;">_______________________________<br>
Client Signature</p>
</div>
</body>
</html>
@page {
size: A4;
margin: 2cm;
@bottom-right { content: counter(page); }
}
body { font: 12pt/1.5 Arial, sans-serif; }
h1, h2, h3 { break-after: avoid; page-break-after: avoid; }
p { orphans: 3; widows: 3; }
table { width: 100%; border-collapse: collapse; }
thead { display: table-header-group; }
tr, .no-break { break-inside: avoid; page-break-inside: avoid; }
img { max-width: 100%; break-inside: avoid; page-break-inside: avoid; }
@media print {
* { float: none !important; }
.no-print { display: none !important; }
}
vh, vw, vmin, vmax are invalidbreak-*) and legacy (page-break-*)base_url parameter| Feature | Status | Notes |
|---|---|---|
| CSS Grid | Not supported | Use tables, flexbox, or block layout instead |
| Flexbox | Partial | Basic flex works; avoid for page-level structure |
| CSS Variables | Supported | var(--color) works — great for themeable templates |
| Gradients | Supported | linear-gradient(), radial-gradient() both work |
| calc() | Supported | calc(100% - 2cm) etc. |
| Transforms | 2D only | rotate(), scale(), translate() work; no 3D |
| Box shadow | Supported | Works as expected |
| Border radius | Supported | Works as expected |
| Opacity | Supported | Works as expected |
| Animations / transitions | Not supported | Static rendering only |
| JavaScript | Not executed | No <canvas>, no dynamic DOM |
position: fixed |
Not supported | Use margin boxes for fixed headers/footers |
position: sticky |
Not supported | Use display: table-header-group for sticky table headers |