This guide covers the CSS used to control PDF pages, including size, margins, breaks, running content, counters, and print layout.
@page rules
The @page rule controls page size, margins, and other page-level properties.
Page size
Set the page dimensions using the size property:
@page {
size: A4; /* Standard paper sizes */
}
@page {
size: Letter; /* US Letter (8.5in x 11in) */
}
@page {
size: 210mm 297mm; /* Explicit dimensions (width height) */
}
@page {
size: A4 landscape; /* Orientation modifier */
}Common paper sizes:
| Size | Dimensions | Common use |
|---|---|---|
| A4 | 210mm x 297mm | International standard |
| Letter | 8.5in x 11in | US standard |
| Legal | 8.5in x 14in | Legal documents |
| A3 | 297mm x 420mm | Large format |
| A5 | 148mm x 210mm | Booklets |
Page margins
Control the printable area with margins:
@page {
size: A4;
margin: 2cm; /* All sides */
}
@page {
margin: 2cm 1.5cm; /* Top/bottom, left/right */
}
@page {
margin: 2cm 1.5cm 3cm 1.5cm; /* Top, right, bottom, left */
}
@page {
margin-top: 2.5cm;
margin-right: 2cm;
margin-bottom: 3cm;
margin-left: 2cm;
}Named pages
Target specific pages with named page rules for different layouts within a single document:
/* Define different page styles */
@page cover {
margin: 0;
size: A4;
}
@page content {
margin: 2cm;
size: A4;
}
@page landscape-chart {
size: A4 landscape;
margin: 1.5cm;
}
/* Apply to elements */
.cover-page {
page: cover;
}
.content-section {
page: content;
}
.chart-page {
page: landscape-chart;
}Left and right pages
Style left and right pages differently for book-style layouts:
@page :left {
margin-left: 3cm;
margin-right: 2cm;
}
@page :right {
margin-left: 2cm;
margin-right: 3cm;
}First page
Apply special styling to the first page:
@page :first {
margin-top: 5cm; /* Extra space for title */
}
/* Or combined with named pages */
@page report:first {
@top-center {
content: none; /* No header on first page */
}
}Page breaks
Force page breaks
/* Force a break before an element */
.chapter {
break-before: page;
page-break-before: always; /* Legacy syntax */
}
/* Force a break after an element */
.section-end {
break-after: page;
page-break-after: always;
}Prevent page breaks
/* Never break inside this element */
.keep-together {
break-inside: avoid;
page-break-inside: avoid;
}
/* Prevent breaks between heading and following content */
h1, h2, h3 {
break-after: avoid;
page-break-after: avoid;
}Break values
| Value | Description |
|---|---|
auto | Default behavior |
avoid | Prevent break if possible |
always | Always insert a page break |
page | Force a page break |
left | Break and go to next left page |
right | Break and go to next right page |
Example
/* Document structure */
.chapter {
break-before: page;
}
h1 {
break-after: avoid; /* Keep with following content */
}
table {
break-inside: avoid; /* Don't split tables */
}
tr {
break-inside: avoid; /* Don't split table rows */
}
figure {
break-inside: avoid; /* Keep images with captions */
}
blockquote {
break-inside: avoid;
}Headers and footers
CSS page margin boxes allow you to place content in the margins of every page.
Margin box positions
The page margin is divided into 16 boxes:
┌──────────────────────────────────────────┐
│ @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 │
└──────────────────────────────────────────┘Add headers
@page {
@top-center {
content: "Document Title";
font-size: 10pt;
color: #666;
}
}
@page {
@top-left {
content: "Chapter Name";
}
@top-right {
content: "Section Name";
}
}Add footers
@page {
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
}
}
@page {
@bottom-left {
content: "Confidential";
font-size: 8pt;
color: #999;
}
@bottom-right {
content: "© 2024 Company Name";
font-size: 8pt;
}
}Style margin boxes
@page {
@top-center {
content: "Report Title";
font-family: Arial, sans-serif;
font-size: 11pt;
font-weight: bold;
color: #333;
border-bottom: 1px solid #ccc;
padding-bottom: 5pt;
}
@bottom-center {
content: counter(page);
font-size: 10pt;
padding-top: 5pt;
border-top: 0.5pt solid #ccc;
}
}Running elements
The content property in margin boxes accepts strings, counters, string(), element(), and url(). For logos, styled layouts, or other HTML in a header or footer, combine position: running() with element():
/* Remove the element from the flow and register it */
.running-header {
position: running(headerContent);
}
/* Place it in a margin box */
@page {
margin-top: 3cm;
@top-center {
content: element(headerContent);
}
}<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...</p>Behavior:
- The element is removed from the normal document flow (it won’t appear inline)
- It renders in the margin box on every page
- If a new element with the same running name appears later in the document, it replaces the previous one from that page onward
Replacing the running element lets a document change headers by chapter:
.chapter-header { position: running(chapterHead); }
@page { @top-left { content: element(chapterHead); } }<section>
<div class="chapter-header">Chapter 1: Introduction</div>
<p>Content...</p>
</section>
<section class="chapter">
<div class="chapter-header">Chapter 2: Analysis</div>
<p>Content...</p>
</section>Choose an approach:
| Feature | string-set / string() | running() / element() |
|---|---|---|
| Plain text | Yes | Yes |
| Images / logos | No | Yes |
| Styled multi-element HTML | No | Yes |
| Simpler syntax | Yes | No |
Page counters
Use CSS counters to display page numbers and total page counts.
Basic page numbers
@page {
@bottom-center {
content: counter(page);
}
}Page X of Y
@page {
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
}
}Roman numerals and letters
/* Front matter with roman numerals */
@page frontmatter {
@bottom-center {
content: counter(page, lower-roman); /* i, ii, iii, iv... */
}
}
/* Alternative formats */
content: counter(page, upper-roman); /* I, II, III, IV... */
content: counter(page, lower-alpha); /* a, b, c, d... */
content: counter(page, upper-alpha); /* A, B, C, D... */Custom counter styles
/* Reset page counter for main content */
.main-content {
counter-reset: page 1;
}
/* Section-based page numbers */
@page {
@bottom-center {
content: "Section " counter(section) " - Page " counter(page);
}
}
body {
counter-reset: section;
}
.section {
counter-increment: section;
}Widows and orphans
Prevent single lines from appearing alone at the top or bottom of pages.
p {
widows: 3; /* Minimum lines at top of new page */
orphans: 3; /* Minimum lines at bottom before break */
}
/* Stricter control for important content */
.important-paragraph {
widows: 4;
orphans: 4;
}Definitions:
- Widows: Lines left alone at the top of a new page
- Orphans: Lines left alone at the bottom of a page before a break
@media print
Use the print media query to apply styles only when generating PDFs or printing.
Basic usage
/* Screen-only styles */
@media screen {
.nav-menu {
display: flex;
}
}
/* Print-only styles */
@media print {
.nav-menu {
display: none;
}
body {
font-size: 12pt;
line-height: 1.4;
}
}Hide screen elements
@media print {
/* Hide navigation and interactive elements */
nav,
.sidebar,
.social-share,
.comments,
.footer-links,
button:not(.print-include),
input,
.no-print {
display: none !important;
}
/* Remove backgrounds for cleaner print */
* {
background: transparent !important;
}
}Link handling
@media print {
/* Show URLs after links */
a[href^="http"]:after {
content: " (" attr(href) ")";
font-size: 0.9em;
color: #666;
}
/* But not for internal links */
a[href^="#"]:after,
a[href^="/"]:after {
content: none;
}
}Print layouts
Invoice or report
@page {
size: A4;
margin: 2cm 1.5cm 3cm 1.5cm;
@top-right {
content: "Invoice #" attr(data-invoice-number);
font-size: 10pt;
}
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
color: #666;
}
}
.invoice-header {
break-after: avoid;
}
.line-items {
break-inside: avoid;
}
.line-items tr {
break-inside: avoid;
}
.invoice-total {
break-before: avoid;
break-inside: avoid;
}Book or long document
@page {
size: 6in 9in; /* Trade paperback size */
margin: 0.75in 0.5in;
}
@page :left {
margin-left: 0.75in;
margin-right: 0.5in;
@bottom-left {
content: counter(page);
}
}
@page :right {
margin-left: 0.5in;
margin-right: 0.75in;
@bottom-right {
content: counter(page);
}
}
@page :first {
@bottom-left {
content: none; /* No page number on title page */
}
}
.chapter {
break-before: right; /* Chapters start on right pages */
}
h1 {
break-after: avoid;
}
img {
max-width: 100%;
break-inside: avoid;
}Multi-column layout
.content {
columns: 2;
column-gap: 1.5em;
column-rule: 1px solid #ddd;
}
/* Prevent column breaks inside elements */
.content p {
break-inside: avoid-column;
}
.content h2 {
column-span: all; /* Headings span all columns */
break-after: avoid;
}Charts
The PDF renderer does not execute JavaScript. Chart.js, D3.js, Plotly.js, Highcharts, and ApexCharts therefore do not render, and <canvas> elements remain blank. Render charts as SVG or images before embedding them in HTML.
Inline SVG
SVG is the recommended chart format because it scales without losing sharpness and can be styled with CSS:
<div class="chart">
<svg viewBox="0 0 400 220" xmlns="http://www.w3.org/2000/svg">
<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"/>
<text x="40" y="215" text-anchor="middle" font-size="10">Q1</text>
<text x="100" y="215" text-anchor="middle" font-size="10">Q2</text>
<text x="160" y="215" text-anchor="middle" font-size="10">Q3</text>
<text x="220" y="215" text-anchor="middle" font-size="10">Q4</text>
<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>Embedded images
Use base64-encoded or hosted chart images:
<!-- Base64 -->
<img src="data:image/png;base64,iVBORw0KGgo..." alt="Chart" style="width:100%; max-width:600px;" />
<!-- Hosted URL -->
<img src="https://example.com/charts/revenue.png" alt="Chart" style="width:100%; max-width:600px;" />Chart CSS
.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 doesn't inherit document styles */
.chart svg text {
font-family: Arial, sans-serif;
font-size: 10pt;
fill: #333;
}Plotly with Python
With Python, Plotly can export charts as SVG or PNG without a browser:
import plotly.graph_objects as go
import plotly.io as pio
import base64
fig = go.Figure(data=[
go.Bar(x=["Q1", "Q2", "Q3", "Q4"], y=[120, 180, 240, 200],
marker_color="#3498db")
])
fig.update_layout(
title="Quarterly 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 — embed directly in HTML
svg_str = pio.to_image(fig, format="svg").decode("utf-8")
# Or as base64 PNG
png_b64 = base64.b64encode(
pio.to_image(fig, format="png", scale=2)
).decode("utf-8")Then embed in your HTML:
<!-- Inline SVG (preferred) -->
<div class="chart">{svg_str}</div>
<!-- Or base64 image -->
<div class="chart">
<img src="data:image/png;base64,{png_b64}" alt="Revenue" />
</div>Requires
pip install plotly kaleido
Other rendering options
| Approach | How |
|---|---|
| Server-side SVG | Generate SVG with D3 (Node.js) or matplotlib (Python) and embed inline |
| Export from JS library | Use Chart.js .toBase64Image(), Plotly .toImage(), or D3 .node().outerHTML to export before sending HTML |
| Static image service | Use QuickChart.io or similar to generate chart images from a URL |
| matplotlib / seaborn | plt.savefig(buf, format='svg') and embed the SVG string |
Chart issues
| Issue | Solution |
|---|---|
| Canvas is blank | <canvas> requires JS; use SVG or images instead |
| Chart overflows page | Set max-width: 100% and use viewBox on SVGs |
| SVG text looks wrong | Set font-family and font-size explicitly on SVG <text> elements |
| Chart splits across pages | Wrap in a container with break-inside: avoid |
| Colors look washed out | Use print-friendly colors with high contrast; avoid transparency |
PDF bookmarks
PDF bookmarks create a navigable outline in the PDF viewer sidebar. Use the -pdfmcp-bookmark-* properties to control the document outline:
/* Assign heading levels to the bookmark tree */
h1 { -pdfmcp-bookmark-level: 1; }
h2 { -pdfmcp-bookmark-level: 2; }
h3 { -pdfmcp-bookmark-level: 3; }
/* Custom bookmark labels */
h1 { -pdfmcp-bookmark-label: content(text); }
/* Collapse sub-levels by default */
h2 { -pdfmcp-bookmark-state: closed; }Properties
| Property | Values | Description |
|---|---|---|
-pdfmcp-bookmark-level | none, 1-6 | Heading level in the PDF outline |
-pdfmcp-bookmark-label | content(text), content(before), content(), custom string | Bookmark display text |
-pdfmcp-bookmark-state | open, closed | Whether children are expanded by default |
Example
/* Full document outline */
h1 { -pdfmcp-bookmark-level: 1; -pdfmcp-bookmark-state: open; }
h2 { -pdfmcp-bookmark-level: 2; -pdfmcp-bookmark-state: closed; }
h3 { -pdfmcp-bookmark-level: 3; }
/* Custom label for appendix */
.appendix h2 {
-pdfmcp-bookmark-level: 1;
-pdfmcp-bookmark-label: "Appendix: " content(text);
}
/* Exclude from bookmarks */
.no-bookmark { -pdfmcp-bookmark-level: none; }Cross-references
Use target-counter() to insert the page number of an element by its id, including in tables of contents and “see page X” references:
/* "See page 5" style references */
a.page-ref::after {
content: " (page " target-counter(attr(href), page) ")";
}<p>For details, see <a class="page-ref" href="#chapter3">Chapter 3</a>.</p>
<!-- Renders: "For details, see Chapter 3 (page 5)." -->Combine it with leaders for a full table of contents. See Table of contents.
Footnotes
CSS footnotes automatically move content to the bottom of the page with proper numbering:
/* Move footnote content to page bottom */
.footnote {
float: footnote;
}
/* Style the in-text reference number */
::footnote-call {
content: counter(footnote);
font-size: 0.7em;
vertical-align: super;
line-height: none;
}
/* Style the footnote marker at page bottom */
::footnote-marker {
content: counter(footnote) ". ";
font-weight: bold;
}
/* Optional: horizontal rule above footnotes */
@page {
@footnote {
border-top: 1px solid #ccc;
padding-top: 0.5em;
}
}<p>This is a statement that needs a source.<span class="footnote">Smith et al., 2024, Journal of Examples.</span></p>Hyphenation
Enable automatic hyphenation to improve text justification and reduce ragged edges:
body {
hyphens: auto;
-webkit-hyphens: auto;
}Important: The lang attribute must be set on the <html> element for hyphenation to work:
<html lang="en">| Property | Values | Description |
|---|---|---|
hyphens | none, manual, auto | Hyphenation mode |
overflow-wrap | normal, break-word | Break long words that overflow |
Preserve backgrounds
By default, browsers strip background colors for printing. Force backgrounds to appear:
body {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* Or on specific elements */
.colored-header {
background: #2c3e50;
color: white;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}Box decoration across pages
Control how borders, padding, and backgrounds behave when an element splits across pages:
.callout-box {
box-decoration-break: clone; /* Repeat decoration on each fragment */
-webkit-box-decoration-break: clone;
border: 2px solid #3498db;
padding: 1em;
background: #f0f8ff;
}| Value | Behavior |
|---|---|
slice (default) | Decoration is sliced at the break; the top fragment has no bottom border and the bottom fragment has no top border |
clone | Each fragment repeats the borders, padding, and background |
Table of contents with leaders
Use leader() with target-counter() to create classic TOC entries with dot leaders:
.toc a {
text-decoration: none;
color: inherit;
display: block;
}
.toc a::after {
content: leader(".") target-counter(attr(href), page);
}
/* Indent sub-levels */
.toc .level-2 { padding-left: 1.5em; }
.toc .level-3 { padding-left: 3em; }<nav class="toc">
<a class="level-1" href="#intro">Introduction</a>
<a class="level-2" href="#background">Background</a>
<a class="level-1" href="#methods">Methods</a>
</nav>
<!-- Renders as:
Introduction ............ 1
Background ............ 3
Methods ................. 5
-->Leader styles
| Style | Syntax | Result |
|---|---|---|
| Dots | leader(".") | Introduction ............ 1 |
| Dashes | leader("-") | Introduction ------------ 1 |
| Underscores | leader("_") | Introduction ____________ 1 |
| Spaces | leader(" ") | Introduction 1 |
CSS support
Paged PDF rendering supports a subset of CSS:
| Feature | Support | Notes |
|---|---|---|
| CSS Grid | Supported for simple cases | Test complex grids and page breaks |
| Flexbox | Partial | Basic flex works; avoid for page-level structure |
| CSS Variables | Supported | var(--color) works normally |
| Gradients | Supported | linear-gradient, radial-gradient both work |
calc() | Supported | Works in all contexts |
| 2D Transforms | Supported | rotate(), scale(), translate(); no 3D |
| 3D Transforms | Not supported | No perspective, rotateX/Y/Z, etc. |
| Box Shadow | Not supported | Use borders or background colors instead |
| Border Radius | Supported | Works normally |
| Opacity | Supported | Works normally |
| Animations / Transitions | Not supported | Static rendering only |
| JavaScript | Not executed | No <script>, no dynamic rendering |
position: fixed | Supported | Fixed elements repeat across pages |
position: sticky | Not supported | Use thead { display: table-header-group; } for sticky headers |
| Viewport units | Not supported | vw, vh, vmin, vmax don’t work; use mm, cm, in, pt |
Rendering notes
Typography
- Use pt (points) for font sizes in print:
font-size: 11pt; - Set appropriate line height:
line-height: 1.4to1.6 - Use print-friendly fonts: Georgia, Times New Roman, or embed custom fonts
- Consider increasing font size slightly for better readability
Images and graphics
img {
max-width: 100%;
height: auto;
break-inside: avoid;
}
figure {
break-inside: avoid;
margin: 1em 0;
}
figcaption {
font-size: 0.9em;
text-align: center;
}Tables
table {
width: 100%;
border-collapse: collapse;
break-inside: auto;
}
thead {
display: table-header-group; /* Repeat on each page */
}
tr {
break-inside: avoid;
}
th, td {
border: 0.5pt solid #333;
padding: 6pt 8pt;
}Colors
- Use enough contrast for printed text and graphics
- Avoid light colors that may not reproduce well
- Use
color-adjust: exactto preserve background colors:
.colored-box {
background-color: #f0f0f0;
color-adjust: exact;
-webkit-print-color-adjust: exact;
}Performance
- Inline critical CSS to avoid external requests
- Use base64-encoded images for small graphics
- Minimize CSS complexity for faster rendering
- Test with multi-page documents to verify pagination
Common patterns
Prevent content splits
/* Keep headings with following content */
h1, h2, h3, h4, h5, h6 {
break-after: avoid;
}
/* Keep code blocks together */
pre, code {
break-inside: avoid;
}
/* Keep list items together */
li {
break-inside: avoid;
}
/* Keep figures together */
figure, .figure {
break-inside: avoid;
}Section separators
.chapter {
break-before: page;
}
.appendix {
break-before: page;
}
.section {
break-before: auto;
margin-top: 2em;
}Full-bleed pages
@page full-bleed {
margin: 0;
}
.full-page-image {
page: full-bleed;
width: 100%;
height: 100%;
object-fit: cover;
}Renderer compatibility
For predictable output across renderers:
/* Use both modern and legacy syntax */
.keep-together {
break-inside: avoid;
page-break-inside: avoid; /* Legacy */
}
.new-page {
break-before: page;
page-break-before: always; /* Legacy */
}Related documentation
- HTML to PDF: convert styled HTML to PDF
- Quick start: make the first API request
- MCP server: connect an AI agent
Test your styles
- Start with a simple document and add complexity gradually
- Test with multi-page content to verify pagination
- Check page breaks at different content lengths
- Verify headers/footers appear correctly on all pages
- Test named pages for different layout sections