# CSS print styling

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:

```css
@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:

```css
@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:

```css
/* 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:

```css
@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:

```css
@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

```css
/* 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

```css
/* 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

```css
/* 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

```css
@page {
  @top-center {
    content: "Document Title";
    font-size: 10pt;
    color: #666;
  }
}

@page {
  @top-left {
    content: "Chapter Name";
  }
  @top-right {
    content: "Section Name";
  }
}
```

### Add footers

```css
@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

```css
@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()`:

```css
/* 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);
  }
}
```

```html
<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:

```css
.chapter-header { position: running(chapterHead); }

@page { @top-left { content: element(chapterHead); } }
```

```html
<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

```css
@page {
  @bottom-center {
    content: counter(page);
  }
}
```

### Page X of Y

```css
@page {
  @bottom-right {
    content: "Page " counter(page) " of " counter(pages);
  }
}
```

### Roman numerals and letters

```css
/* 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

```css
/* 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.

```css
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

```css
/* 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

```css
@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

```css
@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

```css
@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

```css
@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

```css
.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:

```html
<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:

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

```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:

```python
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:

```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:

```css
/* 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

```css
/* 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:

```css
/* "See page 5" style references */
a.page-ref::after {
  content: " (page " target-counter(attr(href), page) ")";
}
```

```html
<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](#table-of-contents-with-leaders).

---

## Footnotes

CSS footnotes automatically move content to the bottom of the page with proper numbering:

```css
/* 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;
  }
}
```

```html
<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:

```css
body {
  hyphens: auto;
  -webkit-hyphens: auto;
}
```

**Important:** The `lang` attribute must be set on the `<html>` element for hyphenation to work:

```html
<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:

```css
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:

```css
.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:

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

```html
<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.4` to `1.6`
- Use print-friendly fonts: Georgia, Times New Roman, or embed custom fonts
- Consider increasing font size slightly for better readability

### Images and graphics

```css
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

```css
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: exact` to preserve background colors:

```css
.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

```css
/* 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

```css
.chapter {
  break-before: page;
}

.appendix {
  break-before: page;
}

.section {
  break-before: auto;
  margin-top: 2em;
}
```

### Full-bleed pages

```css
@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:

```css
/* 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](https://pdf-mcp.io/docs/api-html-to-pdf): convert styled HTML to PDF
- [Quick start](https://pdf-mcp.io/docs/quick-start): make the first API request
- [MCP server](https://pdf-mcp.io/docs/mcp-server): connect an AI agent

## Test your styles

1. Start with a simple document and add complexity gradually
2. Test with multi-page content to verify pagination
3. Check page breaks at different content lengths
4. Verify headers/footers appear correctly on all pages
5. Test named pages for different layout sections
