← pdf-mcp.io

CSS Print Styling Guide

Master CSS paged media for professional PDF generation.

@page Rule

Control page dimensions and margins:

@page {
  size: A4;              /* Paper size */
  margin: 2cm;           /* Content margins */
}

Page Sizes

@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 Selectors

@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 */

Named Pages

@page cover { margin: 0; }
@page chapter { margin: 2.5cm; }
@page appendix { size: A4 landscape; }

.cover { page: cover; }
.chapter { page: chapter; }
.appendix { page: appendix; }

Margin Boxes (Headers/Footers)

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;
  }
}

All Margin Box Positions

@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

Margin Box with Images

@page {
  @top-left {
    content: url("https://example.com/logo.png");
    height: 30px;
  }
}

Running Elements (Rich HTML in Headers/Footers)

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 Counters

@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) ": "; }

Page Breaks

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;
}

Break Values

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

Critical: Reset Floats

Page breaks only work on block-level elements:

@media print {
  * { float: none !important; }
}

Widows and Orphans

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;
}

Tables: Multi-Page Handling

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;
}

Table Continuation Indicator

thead::after {
  content: "(continued)";
  display: none;
}
@media print {
  thead::after { display: table-cell; }
}

Charts and Data Visualizations

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.

Recommended: Inline SVG

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>

Alternative: Embedded Images

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;" />

Styling Charts for Print

.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;
}

Prerendering Strategies

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

Agent Workflow: Plotly + Python

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 kaleido package for static export: pip install plotly kaleido

Common Pitfalls

  1. No <canvas> rendering — Canvas requires JavaScript; it will be blank
  2. No JS-based animations — Static output only
  3. SVG font-family — Use web-safe fonts; custom fonts must be embedded or available on the server
  4. SVG dimensions — Always set viewBox for proper scaling; avoid fixed width/height in px
  5. Large charts — Break into multiple smaller charts rather than one oversized visualization
  6. Colors — Use print-friendly colors with sufficient contrast; avoid relying on transparency

PDF Bookmarks (Document Outline)

Auto-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.

Cross-References (Target Counters)

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>

Footnotes

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>

Hyphenation

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;
}

Multi-Column Layout

.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; }

Preserve Background Colors

Backgrounds may be stripped in print. Force them to render:

.colored-box {
  background: #f0f0f0;
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}

Custom Fonts

@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.

Box Decoration Break

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.

Table of Contents with Leaders

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.

@media print

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;
  }
}

Complete Example: Report with Headers/Footers

<!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>

Complete Example: Legal Document with Numbered Pages

<!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>

Quick Reference

Essential Print CSS Template

@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; }
}

Common Pitfalls

  1. No viewport units in @page - vh, vw, vmin, vmax are invalid
  2. Always reset floats - Page breaks fail on floated elements
  3. Use both break properties - Modern (break-*) and legacy (page-break-*)
  4. Apply break-inside to containers - Not just the content inside
  5. Avoid position: fixed - Not supported in paged media
  6. Set explicit dimensions - Percentage heights may not work as expected
  7. Test margin boxes - Complex layouts need verification
  8. Use absolute URLs for images - Or provide base_url parameter

CSS Support Caveats

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

Related