# 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.
```bash
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "
Invoice #042
", "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.
```bash
# 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.
```js
// pdf.mjs (run with: node pdf.mjs)
import { chromium } from "playwright";
const html = `
Invoice #042
Design work$120.00
`;
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.
```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.
```js
await page.pdf({
path: "report.pdf",
format: "A4",
displayHeaderFooter: true,
headerTemplate: "",
footerTemplate: `
Page of
`,
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](https://html-pdf.com/pdf-headers-footers-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.
```js
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/playwright` Docker images help, but they are large.
- **Memory.** Reuse one browser and open a new context or page per job. Close pages in `finally` blocks, 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()`.
```python
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": ''
'Page of
',
},
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.
```bash
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](https://html-pdf.com/puppeteer-html-to-pdf) · [HTML to PDF in Python](https://html-pdf.com/html-to-pdf-python) · [HTML to PDF in Node.js](https://html-pdf.com/html-to-pdf-nodejs) · [API docs](https://html-pdf.com/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 `` and ``. 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.