Playwright HTML to PDF (Node.js and Python)
Playwright HTML to PDF with page.pdf() in Node.js and Python. Covers options, headers, footers, waiting for content, and a no-browser option.
Quick answer. In Playwright, load the HTML with page.setContent(html) (or page.goto(url)) in Chromium, then call page.pdf({ path: "out.pdf", format: "A4", printBackground: true }). If you'd rather not run a browser, one API call does the same job.
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>Invoice #042</h1>", "format": "A4"}' \
-o invoice.pdf
Playwright isn't only for testing. Its page.pdf() method prints a page with Chromium, the same engine as Chrome's "Save as PDF", so it's a solid way to generate PDFs from HTML. This guide covers Node.js and Python, the options that matter, and the production issues to plan for.
Install
This step gets Playwright and its Chromium browser onto your machine.
# Node.js
npm install playwright
npx playwright install chromium
# Python
pip install playwright
playwright install chromium
On a fresh Linux server or container, add --with-deps to the install command. That also installs the system libraries Chromium needs.
PDF generation only works in Chromium. Calling page.pdf() with Firefox or WebKit throws an error.
HTML string to PDF in Node.js
This example turns an HTML string into an A4 PDF with Node.js.
// pdf.mjs (run with: node pdf.mjs)
import { chromium } from "playwright";
const html = `
<html><head><style>
body { font-family: system-ui, sans-serif }
.row { display: flex; justify-content: space-between }
</style></head>
<body>
<h1>Invoice #042</h1>
<div class="row"><span>Design work</span><span>$120.00</span></div>
</body></html>`;
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle" });
await page.pdf({
path: "invoice.pdf",
format: "A4",
printBackground: true,
margin: { top: "15mm", right: "15mm", bottom: "15mm", left: "15mm" },
});
} finally {
await browser.close();
}
HTML or URL to PDF in Python
This example saves a web page as a PDF with Python.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com", wait_until="networkidle")
page.emulate_media(media="screen") # use screen styles instead of @media print
page.pdf(
path="page.pdf",
format="A4",
print_background=True,
margin={"top": "15mm", "bottom": "15mm", "left": "15mm", "right": "15mm"},
)
browser.close()
For an HTML string, use page.set_content(html, wait_until="networkidle") instead of goto. In async code (FastAPI, for example), use playwright.async_api with await.
Options worth knowing
These are the page.pdf() options you'll use most, with their names in each language.
| Node.js | Python | Purpose |
|---|---|---|
format / width, height |
same | Paper size, e.g. "A4", "Letter", or "210mm" |
landscape |
same | Orientation |
margin |
same | { top, right, bottom, left } |
printBackground |
print_background |
Keep background colors and images |
preferCSSPageSize |
prefer_css_page_size |
Let @page { size } decide |
pageRanges |
page_ranges |
e.g. "1-3" |
scale |
same | 0.1 to 2 |
displayHeaderFooter, headerTemplate, footerTemplate |
display_header_footer, header_template, footer_template |
Repeated header and footer |
Page numbers in the footer
This snippet adds "Page X of Y" to the bottom of every page.
await page.pdf({
path: "report.pdf",
format: "A4",
displayHeaderFooter: true,
headerTemplate: "<span></span>",
footerTemplate: `<div style="font-size:9px;width:100%;text-align:center">
Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>`,
margin: { top: "20mm", bottom: "20mm" },
});
Style the templates inline, because they don't see your page's CSS. Also leave a margin, or they have nowhere to render. More in headers, footers and page numbers.
Wait for the content to be ready
This section stops you from printing a page before charts or fonts have loaded.
networkidle waits for network requests to settle, but not for JavaScript that renders afterwards. Wait for a real signal instead.
await page.waitForFunction(() => window.chartsReady === true);
await page.evaluate(() => document.fonts.ready);
Production issues to plan for
Running Playwright for PDFs in production brings a few problems worth planning for.
- Browser binaries everywhere. Each server, container and CI runner needs a matching Chromium. The official
mcr.microsoft.com/playwrightDocker images help, but they are large. - Memory. Reuse one browser and open a new context or page per job. Close pages in
finallyblocks, or long-running workers leak. - Concurrency. A burst of PDF requests means a burst of browser pages. You'll want a queue or a pool.
- Serverless. Bundling Chromium into functions runs into size limits and cold starts.
- Untrusted input. Rendering user HTML or URLs from inside your network can reach internal services unless you block private addresses.
Or skip the browser: one API call
This section shows how to get the same PDF without installing or scaling browsers. html-pdf.com runs headless Chromium for you, so the output matches page.pdf().
import os, requests
r = requests.post(
"https://html-pdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"},
json={
"url": "https://example.com",
"format": "A4",
"margin": "15mm",
"media": "screen",
"wait_for_expression": "window.chartsReady === true",
"footer_html": '<div style="font-size:9px;width:100%;text-align:center">'
'Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
},
timeout=60,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
| Playwright | html-pdf.com |
|---|---|
setContent(html) / goto(url) |
html / url (public pages) |
printBackground |
print_background (on by default) |
headerTemplate / footerTemplate |
header_html / footer_html |
emulateMedia({ media: "screen" }) |
media: "screen" |
waitForFunction(...) |
wait_for_expression |
preferCSSPageSize, pageRanges, scale |
prefer_css_page_size, page_ranges, scale |
There are a few extras. max_pages fails a runaway document without charge, and response: "url" returns a 24-hour download link instead of bytes. Your HTML isn't stored.
Keep Playwright if you need to log in, click through a page, or already run browsers for testing. Use the API when PDFs are the only reason you run Chromium.
curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}'
Then open the link we email you to confirm your address. That adds 100 free credits, and 1 credit = 1 page.
Related: Puppeteer HTML to PDF · HTML to PDF in Python · HTML to PDF in Node.js · API docs
FAQ
How do I convert HTML to PDF with Playwright?
Launch Chromium, load your HTML with page.setContent(html) or a URL with page.goto(url), then call page.pdf() with a path and format. Set printBackground: true to keep background colors.
Why does page.pdf() not work in Firefox or WebKit?
Playwright only supports PDF generation in Chromium. Calling page.pdf() with Firefox or WebKit throws an error.
How do I add page numbers to a Playwright PDF?
Set displayHeaderFooter: true and use a footerTemplate with <span class="pageNumber"></span> and <span class="totalPages"></span>. Style it inline and leave a bottom margin so it has room to render.
Can I generate PDFs without installing Playwright?
Yes. html-pdf.com runs headless Chromium for you, so you send HTML or a URL to POST https://html-pdf.com/v1/pdf and get the PDF back. You get 100 free credits, with no card required.
