Use CSS paged-media rules in the HTML you give to pdfkit: define the normal page margin in @page, then override the first page with @page :first.
@page {
margin: 20mm;
}
@page :first {
margin-top: 35mm;
}
Python pdfkit passes that HTML to the wkhtmltopdf executable. The CSS rule is defined by CSS 2.2, but support depends on the exact wkhtmltopdf build installed on your machine, so render a two-page test and inspect the output before relying on it in production.
What the two margin layers control
There are two separate places to set margins:
| Layer | Typical setting | Scope | Best use |
|---|---|---|---|
| Renderer options | margin-top, margin-right, margin-bottom, margin-left |
Every page in the rendered document | A common baseline supplied from Python to wkhtmltopdf |
| Paged CSS | @page and @page :first |
General page box and first-page override | A first-page-only difference expressed in the stylesheet |
pdfkit is a Python wrapper, not the PDF layout engine. Its documented options map to wkhtmltopdf command-line switches; CSS is interpreted later by wkhtmltopdf’s rendering engine. The wkhtmltopdf usage documentation lists page-wide margin switches but does not document a first-page-specific command-line switch: wkhtmltopdf usage options.
CSS 2.2 defines :first as a page selector and states that declarations in the more specific first-page rule override the general @page rule: W3C paged media specification.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
Install and verify the toolchain
Install Python pdfkit
python -m pip install pdfkit
You also need a wkhtmltopdf executable. Install the package appropriate for your operating system, then verify that it is on your PATH:
wkhtmltopdf --version
Record this version with your deployment configuration. wkhtmltopdf describes its renderer as an older WebKit/Qt stack, and its status page explains the project’s maintenance state: wkhtmltopdf status. A distribution package, a manually downloaded binary and a container image can therefore behave differently.
Point pdfkit at a non-standard executable
If the command is not on PATH, pass its absolute path:
import pdfkit
config = pdfkit.configuration(
wkhtmltopdf="/absolute/path/to/wkhtmltopdf"
)
pdfkit.from_string("<p>test</p>", "test.pdf", configuration=config)
Use a real executable path for your platform. On Windows, for example, this is commonly a path under Program Files; on Linux containers it is often /usr/bin/wkhtmltopdf.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchA complete two-page example
The following example deliberately creates enough content for two pages. The first page gets a 35 mm top margin; subsequent pages use 20 mm on all sides.
Rank #2
HTML and CSS
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
@page {
size: A4;
margin: 20mm;
}
@page :first {
margin-top: 35mm;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: Arial, sans-serif;
font-size: 11pt;
line-height: 1.45;
}
h1 { margin: 0 0 8mm; }
p { margin: 0 0 5mm; }
.page-break { page-break-before: always; }
</style>
</head>
<body>
<h1>Report title</h1>
<p>This content starts lower because the first page has a 35 mm top margin.</p>
<p>Add enough paragraphs, rows or sections to force a second page.</p>
<div class="page-break"></div>
<h2>Second-page section</h2>
<p>This page returns to the 20 mm top margin.</p>
</body>
</html>
Python with pdfkit
from pathlib import Path
import pdfkit
html = Path("report.html").read_text(encoding="utf-8")
options = {
"page-size": "A4",
"encoding": "UTF-8",
"print-media-type": None,
"quiet": None,
}
# Leave the margins in CSS so @page :first owns the distinction.
pdfkit.from_string(html, "report.pdf", options=options)
print("Wrote report.pdf")
The None values above are pdfkit’s way to emit boolean wkhtmltopdf switches. You can omit print-media-type if your stylesheet does not use media queries. If you set margin-top or the other margin options in Python, those values are page-wide renderer settings; they are useful for a common baseline but may affect how a particular binary applies CSS page margins. Keep the test document and verify the result after changing either layer.
Setting a common baseline from Python
Sometimes deployment code owns the paper settings while the template owns the first-page adjustment. You can pass the common values through pdfkit:
options = {
"page-size": "A4",
"margin-top": "20mm",
"margin-right": "20mm",
"margin-bottom": "20mm",
"margin-left": "20mm",
"encoding": "UTF-8",
}
pdfkit.from_string(html, "report.pdf", options=options)
Then retain the @page :first rule in the stylesheet and test the installed wkhtmltopdf version. If the first-page difference disappears, remove the conflicting per-side command-line margin and let CSS provide both the baseline and the override, or treat the renderer as lacking reliable support for this rule.
Margins, padding and headers are not the same thing
Page-box margin
@page controls the printable page box. It changes where the document’s page area begins, including the first-page override.
Body and element spacing
Rules such as body { margin: 8px; }, heading margins, container padding and table spacing are ordinary content-box styles. They add whitespace inside the page area and can make a page-margin change appear too small or too large. Reset them while diagnosing:
html, body {
margin: 0;
padding: 0;
}
Headers and footers
wkhtmltopdf header and footer options reserve their own space. A header can make the first page’s content look lower even when the page margin is identical. Test with headers and footers disabled, establish the page-margin behavior, then add them back and adjust their spacing independently.
Verify first-page support on your actual binary
- Save the sample HTML as
margin-test.html. - Confirm the file necessarily produces at least two pages; use a forced page break or substantial text.
- Run
wkhtmltopdf --versionand save the output with your build artifacts. - Generate the PDF through the same Python code and executable used in deployment.
- Open the PDF and compare the distance from the physical top edge to the first-page heading and to the second-page heading.
- Repeat on every operating-system image or container that will render documents.
This is a regression check you can keep in CI as a visual or coordinate comparison. It is especially important because the wkhtmltopdf issue tracker contains a report of a first-page top-margin discrepancy: issue #3820. That report is evidence of a problem in a particular implementation, not a universal compatibility result.
Free tools Windows power users keep installed
One-click scans. No signup required.
Troubleshooting
The first page looks identical to later pages
- Confirm the document has two pages; a one-page render cannot show a difference.
- Reset
body, headings and wrapper padding to zero while testing. - Check that the selector is exactly
@page :first, with a space before:first. - Remove Python
margin-*options temporarily so you can determine whether command-line settings are taking precedence. - Test the same HTML with the exact wkhtmltopdf binary from production. Standards support does not guarantee implementation support in an older build.
There is unexpected whitespace only around a heading
Inspect the heading’s own margin-top, the first child’s collapsing margin and any wrapper padding. These are content styles, not page-box margins. Set explicit values such as h1 { margin: 0 0 8mm; } while isolating the issue.
The script raises “No wkhtmltopdf executable found”
Install wkhtmltopdf, add it to PATH, or pass an absolute path through pdfkit.configuration(). Run that path directly in a shell to catch permissions or missing shared-library errors.
The PDF is blank or conversion exits with an error
- Run without
quietso wkhtmltopdf’s diagnostic output is visible. - Use a local HTML file first to separate renderer problems from network, JavaScript or asset-loading problems.
- Check that referenced fonts, images and stylesheets are readable by the rendering process.
- For local files, enable the wkhtmltopdf local-file option only when required by your document and deployment policy.
The first page is correct but later content shifts unexpectedly
Look for a large first-page-only element, a forced page break, oversized header, or content that cannot fit in the remaining page box. A margin changes available content height; it can legitimately move a block to the next page.
Choosing values and keeping renders stable
Use physical units
Use mm, cm or in for print margins. They communicate the intended paper geometry more clearly than pixels and avoid tying the template to a screen density.
Recommended Free Tools
Keep the distinction in one place
Put the normal and first-page values together in the same @page block. Keep Python options for paper size, encoding, orientation and other deployment-level choices unless you have verified how your wkhtmltopdf version combines them with CSS.
Expect layout cost from a larger first margin
A 35 mm first-page top margin leaves less vertical room than a 20 mm margin. Long headings, tables and images may move to page two. If page count matters, test with the largest realistic title, logo and introductory content rather than a minimal placeholder.
Cache and compare deterministic inputs
For reliable regression checks, pin the wkhtmltopdf binary, fonts, CSS and input data. A changed font or image dimension can look like a margin regression even when the page rule is unchanged.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your goal is a clean visual capture of a rendered web page rather than a print-oriented PDF, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; failed loads, bot checks or blank pages are not billed, and each response reports the page verdict and billing status in headers.
One GET request is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for options such as PNG, JPEG or WebP output, full-page capture with lazy images loaded, CSS-selector element capture, device and retina settings, custom CSS or JavaScript, waiting for a selector or network idle, request blocking, cookies and headers, timezone and geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks and bulk capture of up to 100 URLs per call.
Best Value
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo’s MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots, with every feature available on every plan. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can I convert a URL instead of an HTML string?
Yes. Use pdfkit’s from_url() method and keep the same @page rules in the page’s stylesheet. Ensure the renderer can reach the stylesheet and any fonts or images before judging the margin result.
Do I need a separate CSS file for the first-page rule?
No. The rule can be inline in the document passed to pdfkit or loaded from a stylesheet. Inline CSS is often simplest for a self-contained regression fixture.
Is a larger first-page margin suitable for every paper size?
Not automatically. Recheck the available content height when changing between A4, Letter or custom sizes; the same millimetre value can push different content onto a new page.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




