Print CSS guide
Page size and margins, page breaks, repeating table headers, running headers, page numbers, fonts and external resources, and when to use fast or print mode.
On this page
CastPDF renders your HTML in Chromium and prints it to PDF. Everything you know about CSS works; this page covers the print-specific parts and what the renderer adds.
Fast and print mode
fast | print | |
|---|---|---|
| How it paginates | Chromium's own print engine | Paged.js paginates the page first, then Chromium prints it |
| Default for | Raw HTML (html) | Templates (template_id) |
| Page size, margins, page breaks, backgrounds | Yes | Yes |
Running headers from the content (string-set) | No | Yes |
Page numbers in a table of contents (target-counter) | No | Yes |
Repeating <thead> on continued pages | Chromium's built-in behaviour | CastPDF's Paged.js handler (below) |
CastPDF's fixes for inline and <body> break rules (below) | Not needed | Yes |
Use print for documents with running headers, cross-references or long tables. fast skips the pagination step, so it is quicker, especially for long documents; it suits documents that need none of the print-only features. Set the mode per request ("mode": "print") or in a template's settings.
Page size and margins
The request options format, orientation and margins (or a template's settings) become an @page rule that comes before your CSS. Without them, pages are A4 portrait with 20mm margins. A custom size (width and height) in landscape puts its wider side first.
An @page size or margin in your CSS wins over the template settings. Remove it from your CSS if you want the settings (or the API's format and margins) to apply. Keep one only when the template should always have the same page, whatever the settings or the request say:
@page {
size: A4; /* or A5, letter, legal, A4 landscape, or 210mm 297mm */
margin: 25mm 20mm; /* top/bottom, left/right */
}Backgrounds and colours are printed as they appear on screen: CastPDF sets print-background and print-color-adjust: exact for you.
Page breaks
Use the standard break properties. They work in both modes, in stylesheets and in inline style attributes:
h2 { break-before: page; } /* every chapter on a new page */
.cover { break-after: page; } /* nothing else on the cover */
.signature { break-inside: avoid; } /* never split this block */The older page-break-before, page-break-after and page-break-inside work too. In print mode, CastPDF also makes sure break rules are honoured when they are written inline, on elements that have other inline styles, or in a <style> or <link rel="stylesheet"> placed inside <body>: Paged.js alone would miss them.
Keeping rows together
CastPDF adds this rule to every document, in both modes:
tr, img, figure, .keep-together { break-inside: avoid; }So table rows and images are not split across pages where it can be avoided, and you can add the class keep-together to any block you want kept on one page (a totals box, a signature, an address):
<div class="keep-together">
<p>Subtotal: {{ subtotal | money: "USD" }}</p>
<p>Tax: {{ tax | money: "USD" }}</p>
<p><strong>Total: {{ total | money: "USD" }}</strong></p>
</div>In print mode, a table row or a keep-together block taller than a page is split across pages, so none of its content is lost. An image, canvas or SVG taller than a page is scaled down to fit on one page. In fast mode, the browser splits them. If print mode still finds content that would be cut off, the request fails with content_lost instead of returning an incomplete PDF.
Print mode cannot split two layouts, and fails with content_lost for them:
- A table inside a table cell that is taller than a page. Put the long table after the outer table, not inside one of its cells.
- A cell with
rowspanthat is taller than a page. Give each line its own row instead of spanning one tall cell over several rows.
Fast mode splits both, if you do not need the print-only features.
Repeating table headers
In print mode, when a table continues onto another page, CastPDF repeats its <thead> (and its <colgroup>) at the top of the continuation. Put the header row in a <thead>:
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Amount</th></tr>
</thead>
<tbody>
{% for line in lines %}
<tr><td>{{ line.description }}</td><td>{{ line.qty }}</td><td>{{ line.amount | money: "USD" }}</td></tr>
{% endfor %}
</tbody>
</table>This is part of the renderer's automated test suite, with a 60-row invoice that spans several pages.
Page numbers and running headers
In print mode, Paged.js supports the @page margin boxes (such as @top-left, @top-right, @bottom-center and @bottom-right), the page and pages counters, and named strings. Margin content lives inside the page margin, so keep the margins large enough for it.
Page X of Y in the footer, but not on the cover:
@page {
margin: 20mm 20mm 25mm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
color: #64748b;
}
}
@page :first {
@bottom-center { content: none; }
}A running header with the current section's title, taken from the document (string-set), and a fixed label on the right:
@page {
margin: 25mm 20mm;
@top-left { content: string(section); font-size: 9pt; font-weight: 700; }
@top-right { content: "ACME Quarterly Review"; font-size: 9pt; }
}
h2 { string-set: section content(text); break-before: page; }Page numbers in a table of contents (target-counter), for links to headings with an id:
nav a::after { content: " p. " target-counter(attr(href), page); }<nav>
<ol>
<li><a href="#results">Results</a></li>
<li><a href="#outlook">Outlook</a></li>
</ol>
</nav>
<h2 id="results">Results</h2>Each of these three examples is covered by the renderer's test suite in print mode. In fast mode, string-set and target-counter have no effect.
Fonts
These font families are installed on the renderer, so you can use them without loading anything:
- Noto (
Noto Sans,Noto Serifand the Noto families for other scripts, including Arabic), the Noto CJK families (such asNoto Sans CJK SCandNoto Serif CJK JP) for Chinese, Japanese and Korean, and Noto Color Emoji; - Liberation Sans, Liberation Serif and Liberation Mono (metric-compatible with Arial, Times New Roman and Courier New);
- DejaVu Sans, DejaVu Serif and DejaVu Sans Mono.
For any other font, load it like on a web page. The renderer waits for fonts to finish loading before it prints. Either link a stylesheet from a public font service, or declare an @font-face with a public https URL:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap">
<style>
body { font-family: Inter, "Noto Sans", sans-serif; }
</style>@font-face {
font-family: "Brand Sans";
src: url("https://cdn.example.com/fonts/brand-sans.woff2") format("woff2");
font-weight: 400;
}Or embed the font file as a data: URL, which needs no network request at all (keep the total size of the rendered HTML in mind):
@font-face {
font-family: "Brand Sans";
src: url("data:font/woff2;base64,d09GMgABAAAAA…") format("woff2");
}Fonts on localhost, on private networks or behind a login cannot be loaded; see below.
Images, fonts and other external resources
The renderer fetches external resources (images, stylesheets, fonts, scripts) itself, under these rules:
- Only
httpandhttpsURLs, on ports 80 and 443, to public internet addresses. Private, loopback and link-local addresses (such as10.x.x.x,192.168.x.x,127.0.0.1or cloud metadata addresses) are refused, including when a public hostname resolves to one, or a redirect leads to one. data:URLs always work and are not counted.- At most 300 requests and 20 MB of downloaded data per document, 10 seconds per request (and never beyond the render time limit), and 5 redirects per request.
- WebSockets, WebRTC and navigating the page elsewhere are not available.
A refused request, or one that fails at the network level (DNS, connection, timeout, size or request limit), does not fail the document: the resource is missing from the PDF, and the response reports it in x-blocked-resources (the number) and, for "response": "url", in blocked_resources (up to 10 URLs with their reasons). An HTTP error answered by the origin server, such as a 404 for a mistyped image URL, is not listed there: the page receives that response as it is. Check your PDFs while you develop, so a missing logo does not go unnoticed.
JavaScript in your HTML runs before the PDF is printed, within the same limits; an error in your script does not stop the render.
Limits
| Limit | Value |
|---|---|
| Pages per document | 50 (more is document_too_large) |
| PDF size | 40 MB (more is document_too_large; large images are the usual cause) |
| Render time | 30 seconds, including loading resources, pagination and printing (more is render_timeout) |
| HTML size after Liquid, with the CSS | 5 MB |
| External resources | See above |
Test keys and previews add a diagonal "CASTPDF TEST" watermark to every page after printing; it does not change the layout.