Back to Blog
Tutorial

CSS Print Stylesheets: The Complete Developer's Reference

January 22, 202612 min read

CSS Print Stylesheets: The Complete Developer's Reference

Every time you generate a PDF from HTML, you're essentially telling the browser (or PDF engine) to "print" your web page onto fixed-size paper. Yet most CSS is written exclusively for screens — infinite scrolling, responsive breakpoints, hover effects. Print CSS is the forgotten sibling.

This article is a practical reference for everything print-related in CSS. Bookmark it — you'll come back to it every time you build a PDF template.

The @media print Query

The simplest way to add print-specific styles is with the @media print query:

/* Styles that only apply when printing (or generating PDF) */
@media print {
    .no-print {
        display: none !important;
    }

    body {
        font-size: 11pt;
        color: #000;
        background: #fff;
    }

    a {
        text-decoration: none;
        color: #000;
    }
}

But for serious PDF generation, @media print is just the beginning. The real power is in CSS Paged Media.

Units: What to Use for Print

Screen CSS uses px, rem, em, and viewport units (vw, vh). Print CSS should use physical units:

Unit Value Use for
pt 1/72 of an inch Font sizes (standard in print)
mm Millimeters Margins, spacing, dimensions
cm Centimeters Larger margins, page-level spacing
in Inches US paper formats, margins
pc Pica (12pt) Typography

Why not px? A pixel has no fixed physical size — it depends on the device's DPI. On a 96 DPI screen, 1px ≈ 0.26mm. On a 300 DPI printer, 1px ≈ 0.085mm. Using mm or pt ensures consistent sizing across devices and PDF engines.

/* ❌ Screen-oriented sizing */
body {
    font-size: 16px;
    padding: 2rem;
}

/* ✅ Print-oriented sizing */
body {
    font-size: 10pt;
    padding: 10mm;
}

Exception: px is acceptable for borders and hairlines where you want the thinnest possible line:

.divider {
    border-bottom: 0.5pt solid #ccc;
}

The @page Rule

The @page rule controls page-level properties: dimensions, margins, and (in supporting engines) margin boxes for headers and footers.

Page Size

@page {
    size: A4;                    /* 210mm × 297mm */
}

@page {
    size: letter;                /* 8.5in × 11in */
}

@page {
    size: A4 landscape;          /* 297mm × 210mm */
}

@page {
    size: 150mm 200mm;           /* Custom dimensions */
}

Standard sizes: A3, A4, A5, B4, B5, letter, legal, ledger.

Page Margins

@page {
    size: A4;
    margin: 20mm;                /* All sides */
}

@page {
    size: A4;
    margin: 15mm 20mm 25mm 20mm; /* Top Right Bottom Left */
}

Named Pages

You can define different page styles for different sections:

@page cover {
    margin: 0;                    /* Full-bleed cover page */
}

@page landscape-data {
    size: A4 landscape;
    margin: 10mm;
}

@page standard {
    size: A4;
    margin: 20mm;
}

.cover-section {
    page: cover;
}

.data-tables-section {
    page: landscape-data;
}

Support: Named pages work in WeasyPrint and Prince, but NOT in Chromium/Puppeteer.

Page Selectors

@page :first {
    margin-top: 40mm;            /* Extra top margin on first page */
}

@page :left {
    margin-left: 25mm;           /* Wider inner margin for binding */
    margin-right: 15mm;
}

@page :right {
    margin-left: 15mm;
    margin-right: 25mm;          /* Wider inner margin for binding */
}

@page :blank {
    /* Style for intentionally blank pages */
    @top-center {
        content: "This page intentionally left blank";
    }
}

Page Break Control

Break Properties

CSS provides three properties for controlling page breaks:

/* break-before: controls breaks BEFORE the element */
h1 {
    break-before: page;          /* Always start on a new page */
}

h1:first-child {
    break-before: auto;          /* Don't add a blank first page */
}

/* break-after: controls breaks AFTER the element */
.chapter-summary {
    break-after: page;           /* New page after each summary */
}

h2 {
    break-after: avoid;          /* Don't leave heading alone at bottom */
}

/* break-inside: controls breaks WITHIN the element */
table {
    break-inside: avoid;         /* Don't split tables across pages */
}

.card {
    break-inside: avoid;         /* Keep cards together */
}

Break Values

Value Meaning
auto Default — engine decides
avoid Try to avoid breaking here
page Force a page break
left Force break so next content is on a left page
right Force break so next content is on a right page
column Force a column break (in multi-column layouts)
avoid-page Avoid page break specifically
avoid-column Avoid column break specifically

Orphans and Widows

p {
    orphans: 3;                  /* At least 3 lines at bottom of page */
    widows: 3;                   /* At least 3 lines at top of page */
}
  • Orphan: A paragraph's first few lines stranded at the bottom of a page
  • Widow: A paragraph's last few lines stranded at the top of a page

Both are typographically undesirable. Setting both to 3 is a safe default.

Page Margin Boxes (Headers/Footers)

CSS Paged Media defines 16 margin boxes around each page. These are used for running headers, footers, page numbers, and other repeating content.

┌─────────────────────────────────────┐
│ @top-left  │ @top-center │ @top-right│
├────────────┼─────────────┼──────────┤
│            │             │          │
│ @left-top  │             │@right-top│
│            │             │          │
│ @left-mid  │  PAGE       │@right-mid│
│            │  CONTENT    │          │
│ @left-bot  │             │@right-bot│
│            │             │          │
├────────────┼─────────────┼──────────┤
│@bottom-left│@bottom-center│@bot-right│
└─────────────────────────────────────┘

Page Numbers

@page {
    @bottom-right {
        content: "Page " counter(page) " of " counter(pages);
        font-size: 8pt;
        color: #999;
    }
}

Running Headers

@page {
    @top-left {
        content: "Company Name";
        font-size: 8pt;
        color: #666;
    }

    @top-right {
        content: string(chapter-title);
        font-size: 8pt;
        font-style: italic;
    }
}

h1 {
    string-set: chapter-title content();
}

The string-set property captures the heading text, and string() retrieves it in the margin box. This gives you dynamic headers that change based on the current chapter.

First Page Without Header/Footer

@page :first {
    @top-left { content: none; }
    @top-right { content: none; }
    @bottom-right { content: none; }
}

Support: Margin boxes work in WeasyPrint and Prince. Chromium/Puppeteer does NOT support them — you have to use Chrome's limited built-in header/footer API instead.

Generated Content for Print

Cross-References

/* Create a link with the URL visible */
a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 8pt;
    color: #666;
}

/* Don't show URL for internal links */
a[href^="#"]::after {
    content: none;
}

Footnotes (Prince and WeasyPrint)

.footnote {
    float: footnote;
    font-size: 8pt;
}

::footnote-call {
    content: counter(footnote);
    font-size: 7pt;
    vertical-align: super;
}

::footnote-marker {
    content: counter(footnote) ". ";
}

Table of Contents with Leaders

.toc-entry a::after {
    content: leader('.') target-counter(attr(href), page);
}

This produces: Chapter 1: Introduction ............... 1

Support: Leaders and target-counter are Prince and WeasyPrint features.

Practical Print Patterns

Pattern 1: The Invoice

@page {
    size: A4;
    margin: 15mm;
}

@page :first {
    margin-top: 10mm;
    @bottom-center { content: none; }
}

@page {
    @bottom-center {
        content: "Page " counter(page) " of " counter(pages);
        font-size: 8pt;
        color: #999;
    }
}

body {
    font-family: 'Inter', sans-serif;
    font-size: 9pt;
    line-height: 1.4;
    color: #1a1a1a;
}

.invoice-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10mm;
    padding-bottom: 5mm;
    border-bottom: 0.5pt solid #e5e5e5;
}

table {
    width: 100%;
    border-collapse: collapse;
    break-inside: auto;
}

thead {
    display: table-header-group;    /* Repeat on every page */
}

th {
    text-align: left;
    padding: 2mm 3mm;
    font-size: 8pt;
    text-transform: uppercase;
    letter-spacing: 0.5pt;
    border-bottom: 1pt solid #1a1a1a;
}

td {
    padding: 2mm 3mm;
    border-bottom: 0.25pt solid #e5e5e5;
}

tr {
    break-inside: avoid;
}

.totals {
    break-inside: avoid;
    margin-top: 5mm;
    text-align: right;
}

Pattern 2: The Report with Chapters

@page {
    size: A4;
    margin: 25mm 20mm 30mm 25mm;

    @top-right {
        content: string(section-title);
        font-size: 8pt;
        color: #666;
    }

    @bottom-center {
        content: counter(page);
        font-size: 8pt;
    }
}

@page :first {
    @top-right { content: none; }
    @bottom-center { content: none; }
}

h1 {
    break-before: page;
    string-set: section-title content();
    font-size: 24pt;
    margin-bottom: 10mm;
}

h1:first-child {
    break-before: auto;
}

h2 {
    break-after: avoid;
    font-size: 16pt;
    margin-top: 8mm;
    margin-bottom: 4mm;
}

h3 {
    break-after: avoid;
}

figure {
    break-inside: avoid;
    text-align: center;
    margin: 5mm 0;
}

figcaption {
    font-size: 8pt;
    font-style: italic;
    color: #666;
}

Pattern 3: Labels and Cards (Multiple Per Page)

@page {
    size: A4;
    margin: 10mm;
}

.label-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 5mm;
}

.label {
    width: 60mm;
    height: 30mm;
    padding: 3mm;
    border: 0.25pt solid #ccc;
    break-inside: avoid;
    font-size: 8pt;
}

Browser/Engine Compatibility Quick Reference

Feature Chromium WeasyPrint Prince
@page size
@page margin
@page :first
@page :left/:right
Named pages (page:)
Margin boxes
counter(page) in margins
break-before: page
break-inside: avoid ⚠️
orphans/widows ⚠️
string-set
float: footnote
leader()
CSS Grid
Flexbox
Multi-column

The Practical Takeaway

If you need full print CSS support (running headers, page counters, named pages), use WeasyPrint (free) or Prince (commercial).

If you need full web CSS support (Grid, complex JS), use Chromium/Puppeteer — but accept the print CSS limitations.

If you want both web-quality rendering and print-quality layout, consider an API service like PDF-API.io that combines the best of both worlds.

Debugging Print CSS

Chrome DevTools Print Emulation

Chrome DevTools can emulate print media:

  1. Open DevTools (F12)
  2. Open the Rendering panel (Cmd/Ctrl + Shift + P → "Show Rendering")
  3. Set "Emulate CSS media type" to "print"

This shows you how your print styles look without actually printing. However, it doesn't show page breaks or margins — for those, use "File → Print → Preview."

Common Debugging Tips

  1. Background colors don't print by default. Add print-color-adjust: exact; (or -webkit-print-color-adjust: exact;) to force backgrounds in print.

  2. Margins are cumulative. The @page margin and body margin both apply. Set body margin to 0 when using @page margins.

  3. Images may not load. When rendering from HTML strings (not URLs), images with relative paths often fail. Use absolute URLs or base64-encoded images.

  4. Viewport width matters. Puppeteer renders at 800px viewport width by default, which may trigger mobile breakpoints. Set viewport width explicitly if needed.

Conclusion

Print CSS is a powerful, underused part of the web platform. For anyone generating PDFs from HTML, mastering @page rules, break properties, and margin boxes is essential. These aren't obscure features — they're the difference between a professional document and one that looks like a screenshot pasted into a PDF.

The CSS Paged Media specification gives you fine-grained control over print layout. The main challenge is browser support — but if you choose the right PDF engine for your needs, you can use these features today.


Building PDF templates with HTML/CSS? PDF-API.io renders your templates with full CSS support and handles the tricky parts — page breaks, fonts, and margins — automatically. Start for free.

Ready to automate your PDFs?

Start generating professional documents in minutes. Free plan includes 100 PDFs/month.

Start for Free