May 11, 2026

wkhtmltopdf @page CSS Guide: size, margin, and Layout

wkhtmltopdfCSSPDF generationtutorial

wkhtmltopdf's @page CSS support is one of its most common pain points. Developers run @page { size: A4; margin: 0; } expecting zero margins, only to find the engine ignores the declaration entirely. This guide explains exactly which @page properties wkhtmltopdf honors, which it ignores, and how to work around the gaps.

If you've already decided to migrate, skip to the migration section — the workarounds here will buy time, but the underlying limitations are architectural.

Why wkhtmltopdf Handles @page Differently

wkhtmltopdf is built on an old version of WebKit (Qt WebKit, frozen around 2012). The CSS @page rule is part of the CSS Paged Media specification — a spec that WebKit never fully implemented. As a result, wkhtmltopdf reads only a subset of @page properties and ignores the rest without any error or warning.

The practical consequence: you cannot rely on @page alone to control page layout. You have to split CSS declarations between the stylesheet and CLI flags.

@page Properties: What Works and What Doesn't

Property wkhtmltopdf support Notes
size: A4 Partial Ignored if conflicting CLI --page-size is set
size: A4 landscape Partial Use --orientation Landscape CLI flag instead
margin: 0 Ignored Must use --margin-* CLI flags
margin: 20mm Ignored Must use --margin-* CLI flags
@top-center { content: ... } Not supported Use --header-html or --footer-html
@bottom-right { content: counter(page) } Not supported Use --footer-right "[page]"
marks: crop cross Not supported
orphans / widows Partial Inconsistent results

Summary: For margins, always use CLI flags. For headers/footers, always use the dedicated HTML options. @page size works as a hint, but defer to --page-size for reliable behavior.

Setting Page Size

A4 (the common case)

/* CSS hint — may work in some wkhtmltopdf versions */
@page {
  size: A4;
}

CLI flag (reliable):

wkhtmltopdf --page-size A4 input.html output.pdf

If both are set and conflict, the CLI flag wins.

Custom size

# 210mm x 297mm (A4 exact)
wkhtmltopdf --page-width 210mm --page-height 297mm input.html output.pdf

# Letter (US)
wkhtmltopdf --page-size Letter input.html output.pdf

Landscape

/* Ignored by most wkhtmltopdf versions */
@page { size: A4 landscape; }
# Reliable
wkhtmltopdf --orientation Landscape --page-size A4 input.html output.pdf

Setting Margins

@page { margin: 0; } does not work in wkhtmltopdf. The engine uses its own default margins (~10mm) regardless of what the CSS says.

Zero margins

wkhtmltopdf \
  --margin-top 0 \
  --margin-right 0 \
  --margin-bottom 0 \
  --margin-left 0 \
  input.html output.pdf

Standard document margins

wkhtmltopdf \
  --margin-top 20mm \
  --margin-right 15mm \
  --margin-bottom 20mm \
  --margin-left 15mm \
  input.html output.pdf

Extra bottom margin for footer

If you're using a footer, add extra bottom margin so content doesn't overlap:

wkhtmltopdf \
  --margin-bottom 25mm \
  --footer-html footer.html \
  --footer-spacing 5 \
  input.html output.pdf

Page Numbers

wkhtmltopdf provides page numbers through CLI substitution variables, not CSS counters.

Simple page number in footer

wkhtmltopdf \
  --footer-right "Page [page] of [topage]" \
  --footer-font-size 9 \
  input.html output.pdf

Available variables: [page], [topage], [date], [time], [title], [doctitle], [sitepage], [sitepages].

HTML footer with page numbers

For styled footers, use a separate HTML file with JavaScript:

<!-- footer.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      margin: 0;
      padding: 4mm 15mm;
      font-family: 'Helvetica Neue', sans-serif;
      font-size: 8pt;
      color: #6b7280;
    }
    .footer-inner {
      display: flex;
      justify-content: space-between;
      border-top: 1px solid #e5e7eb;
      padding-top: 3mm;
    }
  </style>
</head>
<body>
  <div class="footer-inner">
    <span>Confidential</span>
    <span id="page-num"></span>
  </div>
  <script>
    // wkhtmltopdf injects these variables into the footer URL
    var vars = {};
    var parts = window.location.search.substr(1).split('&');
    for (var i = 0; i < parts.length; i++) {
      var p = parts[i].split('=', 2);
      if (p.length == 2) vars[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
    }
    document.getElementById('page-num').textContent =
      'Page ' + (vars['page'] || '') + ' of ' + (vars['topage'] || '');
  </script>
</body>
</html>
wkhtmltopdf \
  --footer-html footer.html \
  --margin-bottom 20mm \
  --footer-spacing 3 \
  input.html output.pdf

The --enable-javascript and --javascript-delay flags may be needed if the script doesn't execute reliably:

wkhtmltopdf \
  --enable-javascript \
  --javascript-delay 200 \
  --footer-html footer.html \
  input.html output.pdf

Headers

Headers work the same way as footers:

wkhtmltopdf \
  --header-html header.html \
  --margin-top 25mm \
  --header-spacing 3 \
  input.html output.pdf

Or use the simple text options:

wkhtmltopdf \
  --header-left "[title]" \
  --header-right "[date]" \
  --header-line \
  --header-font-size 9 \
  input.html output.pdf

@page CSS That Does Work

A few @page properties do work reliably:

@page {
  /* Size hint (defer to CLI for reliability) */
  size: A4;

  /* These have no effect — use CLI instead */
  /* margin: 20mm; */
  /* @top-center { ... } */
}

/* break-before / break-after on elements (not inside @page) */
.page-break {
  page-break-before: always;
  break-before: page;
}

/* Prevent breaks inside elements */
.keep-together {
  page-break-inside: avoid;
  break-inside: avoid;
}

Page break properties (page-break-before, page-break-inside) work well in wkhtmltopdf and are the main CSS tool you have for controlling page layout. Use them liberally.

Complete Working Example

A production wkhtmltopdf command with margins, footer, and page size:

wkhtmltopdf \
  --page-size A4 \
  --margin-top 20mm \
  --margin-right 15mm \
  --margin-bottom 25mm \
  --margin-left 15mm \
  --footer-html footer.html \
  --footer-spacing 5 \
  --enable-javascript \
  --javascript-delay 100 \
  --print-media-type \
  --encoding utf-8 \
  --disable-smart-shrinking \
  input.html \
  output.pdf

Key flags explained:

Flag Purpose
--print-media-type Apply @media print CSS rules
--disable-smart-shrinking Prevent content from being scaled down to fit
--encoding utf-8 Required for non-ASCII characters (Japanese, etc.)
--javascript-delay 100 Wait for JS to execute before rendering

Generating from HTML Strings (Subprocess)

When generating from application code, wkhtmltopdf is typically invoked as a subprocess with stdin:

# Pipe HTML via stdin
echo "<html>...</html>" | wkhtmltopdf \
  --page-size A4 \
  --margin-top 20mm \
  --footer-right "Page [page] of [topage]" \
  - output.pdf
import subprocess

html = "<html><body><h1>Invoice</h1></body></html>"

result = subprocess.run(
    [
        "wkhtmltopdf",
        "--page-size", "A4",
        "--margin-top", "20mm",
        "--margin-right", "15mm",
        "--margin-bottom", "20mm",
        "--margin-left", "15mm",
        "--footer-right", "Page [page] of [topage]",
        "--quiet",
        "-",        # stdin
        "-",        # stdout
    ],
    input=html.encode("utf-8"),
    capture_output=True,
)

pdf_bytes = result.stdout
const { execFile } = require("child_process");
const { promisify } = require("util");
const execFileAsync = promisify(execFile);

async function htmlToPdf(html) {
  const args = [
    "--page-size", "A4",
    "--margin-top", "20mm",
    "--footer-right", "Page [page] of [topage]",
    "--quiet",
    "-",  // stdin
    "-",  // stdout
  ];

  const { stdout } = await execFileAsync("wkhtmltopdf", args, {
    input: html,
    encoding: "buffer",
    maxBuffer: 50 * 1024 * 1024,
  });

  return stdout; // Buffer containing PDF bytes
}

Known Limitations

Beyond @page CSS, wkhtmltopdf has other architectural limitations worth knowing:

  • No CSS Grid or Flexbox (partial): Grid is unsupported; Flexbox works partially. Complex layouts often need table-based fallbacks.
  • JavaScript support is limited: Some modern JS APIs don't exist in the embedded WebKit.
  • Font loading: Web fonts from @font-face sometimes fail to load; embed base64 or use system fonts.
  • thead repetition: display: table-header-group works inconsistently — some table structures fail to repeat headers across pages.
  • No active maintenance: The last release was 0.12.6 in 2020. Security vulnerabilities go unpatched.

For a full feature comparison, see wkhtmltopdf vs Chromium PDF engine.

Migrating to a Modern PDF API

If wkhtmltopdf's limitations are causing production problems, migrating to a Chromium-based API resolves most of them with minimal code changes.

With FUNBREW PDF, the same HTML that wkhtmltopdf struggles with works correctly because the underlying engine is Chromium — the same rendering engine as Chrome. The @page CSS spec is fully implemented: margin: 0 works, @top-center margin boxes work, and thead { display: table-header-group; } repeats reliably.

Before (wkhtmltopdf subprocess)

result = subprocess.run(
    ["wkhtmltopdf", "--margin-top", "20mm", "--margin-bottom", "20mm",
     "--footer-right", "Page [page]", "-", "-"],
    input=html.encode("utf-8"),
    capture_output=True,
)
pdf_bytes = result.stdout

After (FUNBREW PDF API)

import requests

response = requests.post(
    "https://pdf.funbrew.cloud/api/pdf/generate",
    headers={"Authorization": "Bearer sk-your-api-key"},
    json={
        "html": html,
        "options": {
            "engine": "quality",    # Chromium — full CSS @page support
            "format": "A4",
            "margin": {
                "top": "20mm",
                "right": "15mm",
                "bottom": "20mm",
                "left": "15mm",
            },
            "displayHeaderFooter": True,
            "footerTemplate": "<div style='font-size:9pt; color:#6b7280; width:100%; text-align:right; padding-right:15mm;'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
        },
    },
)

download_url = response.json()["data"]["download_url"]

Key differences: no subprocess, no CLI flag juggling, margin works directly in the JSON options, and Chromium's @page CSS support means your stylesheet controls layout without workarounds.

For a step-by-step walkthrough of the migration, see the wkhtmltopdf migration guide.

Quick Reference: wkhtmltopdf CLI Flags

Goal CSS (does it work?) CLI flag
A4 page size @page { size: A4 } (partial) --page-size A4
Zero margins @page { margin: 0 } (no) --margin-top 0 --margin-right 0 --margin-bottom 0 --margin-left 0
20mm top margin @page { margin-top: 20mm } (no) --margin-top 20mm
Landscape @page { size: landscape } (no) --orientation Landscape
Page numbers @page { @bottom-right { ... } } (no) --footer-right "Page [page] of [topage]"
Page break page-break-before: always (yes)
Avoid row split break-inside: avoid (yes)
Print media CSS --print-media-type

Debugging Tips

  1. Add --debug-javascript to see JS errors in the footer/header HTML
  2. Test with --dump-outline outline.xml to inspect the page structure
  3. Use --quiet in production to suppress non-error output to stderr
  4. Try Playground for instant HTML-to-PDF feedback without CLI setup — paste your HTML and see the output immediately in your browser

Summary

For wkhtmltopdf @page CSS:

  • Margins: Always use CLI flags (--margin-*), not @page { margin } — the CSS is ignored
  • Page size: CLI --page-size is authoritative; @page { size } is a fallback hint at best
  • Landscape: Use --orientation Landscape, not @page { size: landscape }
  • Page numbers / headers / footers: Use --footer-right, --header-html, --footer-html options
  • Page breaks: page-break-before: always and break-inside: avoid work well — use them freely
  • Background colors: Require --print-media-type and -webkit-print-color-adjust: exact

If you're hitting walls with these workarounds, a Chromium-based API like FUNBREW PDF removes the CLI entirely and gives you full @page CSS support. You can try it in the Playground without signing up.

Related Links

Powered by FUNBREW PDF