How to Generate PDFs with Playwright
Create reliable PDFs with Playwright using print CSS, custom paper sizes, backgrounds, headers, footers, and production troubleshooting.

Playwright generates a PDF with page.pdf(). The method returns a PDF buffer, or writes a file when you provide path. It uses print CSS media by default, so the result can differ from what you see in a normal browser window. A dependable PDF workflow chooses the media mode, paper size, margins, background behavior, color handling, and optional headers or footers explicitly.
This guide explains the complete workflow for generating PDFs with Playwright, including runnable JavaScript, print and screen CSS, page sizing, CSS @page rules, colors, page ranges, headers, footers, reliability, performance, troubleshooting, and an API alternative when you do not want to operate a browser yourself.
1. Install Playwright and create a PDF
Install Playwright and at least one browser engine in your project:
npm install playwright
npx playwright install chromium
The smallest working example opens a page and saves a PDF:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.pdf({ path: 'page.pdf' });
await browser.close();
})();
According to the Playwright Page API, page.pdf() generates a PDF with print CSS media. The path option is optional: omit it when you need the PDF as a buffer for an HTTP response, object storage upload, or further processing.
Return a buffer instead of writing a file
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
});
require('fs').writeFileSync('report.pdf', pdfBuffer);
2. Wait for the page to be ready
A PDF captures the rendered page at the moment page.pdf() runs. Waiting only for the initial HTML response can produce missing images, incomplete charts, or data that has not finished loading.

Start with navigation timing:
await page.goto('https://example.com/report', {
waitUntil: 'networkidle',
timeout: 60_000,
});
For application-specific readiness, wait for a selector or a known condition:
await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
await page.locator('[data-report-ready="true"]').waitFor({ state: 'visible', timeout: 30_000 });
If a chart is drawn asynchronously, wait for its canvas or an application flag. A fixed delay can be useful for a short animation, but a readiness signal is usually more reliable:
await page.waitForFunction(() => window.reportReady === true, null, {
timeout: 30_000,
});
For lazy-loaded images, scroll through the document before producing the PDF:
await page.evaluate(async () => {
await new Promise((resolve) => {
let y = 0;
const step = 600;
const timer = setInterval(() => {
window.scrollBy(0, step);
y += step;
if (y >= document.body.scrollHeight) {
clearInterval(timer);
window.scrollTo(0, 0);
resolve();
}
}, 100);
});
});
3. Print CSS versus screen CSS
Playwright uses print media when creating a PDF. That means rules inside @media print apply, while screen-only rules may not. If the PDF should match the normal web view, emulate screen media before calling page.pdf():
await page.goto('https://example.com');
await page.emulateMedia({ media: 'screen' });
await page.pdf({ path: 'screen-layout.pdf' });
Use print media for invoices, reports, documentation, and other documents designed for paper. Use screen media for dashboards or pages whose responsive layout is intentionally optimized for a display. Decide this explicitly because switching media can change navigation, columns, colors, and visibility.
You can keep PDF-only rules in your stylesheet:
@media print {
.navigation,
.cookie-banner,
.interactive-controls {
display: none;
}
.report {
break-inside: avoid;
}
}
@media screen {
.navigation {
display: flex;
}
}
4. Choose paper size, dimensions, margins, and scale
The PDF API supports named formats such as A4 and Letter, or explicit width and height. Dimensions and margins accept px, in, cm, and mm. Values without units are treated as pixels. If both a format and dimensions are supplied, the named format takes priority.

await page.pdf({
path: 'a4-report.pdf',
format: 'A4',
margin: {
top: '18mm',
right: '16mm',
bottom: '20mm',
left: '16mm',
},
scale: 1,
});
For a custom receipt or label, use dimensions instead of a named format:
await page.pdf({
path: 'receipt.pdf',
width: '80mm',
height: '180mm',
margin: '4mm',
});
The documented defaults are Letter paper, zero margins, and a scale of 1. Scale must be between 0.1 and 2. Reducing scale can make a wide report fit, but it also makes text smaller. Prefer fixing CSS widths and margins before relying on scale.
Let CSS control the page size
If your print stylesheet already defines the exact paper dimensions, add preferCSSPageSize: true. Otherwise, Playwright scales the content to fit the API-selected paper size.
@page {
size: A4 portrait;
margin: 14mm 12mm 18mm;
}
@media print {
body {
margin: 0;
}
}
await page.pdf({
path: 'css-sized.pdf',
preferCSSPageSize: true,
printBackground: true,
});
5. Include backgrounds and preserve colors
Background graphics are disabled by default. Set printBackground: true when the document depends on colored panels, background images, chart fills, or shaded table rows.
await page.pdf({
path: 'branded-report.pdf',
format: 'A4',
printBackground: true,
});
Browsers can adjust colors for print output. When exact brand colors matter, request exact color adjustment in the print stylesheet:
@media print {
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
Color output still depends on the document, fonts, assets, and browser build. Inspect representative PDFs when a brand color or chart palette is part of the document’s meaning.
6. Add headers, footers, and page numbers
Enable templates with displayHeaderFooter: true. Playwright provides special classes for the date, title, URL, current page number, and total page count.
await page.pdf({
path: 'with-footer.pdf',
format: 'A4',
margin: {
top: '22mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
displayHeaderFooter: true,
headerTemplate: `
<div style="width:100%; font-size:9px; padding:0 15mm; color:#666;">
<span class="title"></span>
</div>`,
footerTemplate: `
<div style="width:100%; font-size:9px; padding:0 15mm; color:#666; display:flex; justify-content:space-between;">
<span class="date"></span>
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>`,
});
Template HTML has constraints: scripts are not evaluated, and the page’s styles are not visible inside the header or footer. Put all template styling inline. Reserve enough top and bottom margin for the templates or they can overlap the document.
7. Select pages and control breaks
Use pageRanges to export only selected pages. Examples include 1-5, 8, and 11-13.
await page.pdf({
path: 'appendix.pdf',
format: 'A4',
pageRanges: '11-13',
});
Use CSS break properties to keep related content together:
.invoice-line-items {
break-inside: avoid;
}
.chapter {
break-before: page;
}
.keep-with-next {
break-after: avoid;
}
Long tables need special care. Repeat table headers with display: table-header-group, avoid oversized rows, and ensure cells can wrap:
thead {
display: table-header-group;
}
td, th {
overflow-wrap: anywhere;
}
8. A production-ready Playwright example
const { chromium } = require('playwright');
async function createPdf(url, outputPath) {
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
await page.goto(url, {
waitUntil: 'networkidle',
timeout: 60_000,
});
await page.locator('[data-report-ready="true"]').waitFor({
state: 'visible',
timeout: 30_000,
});
await page.emulateMedia({ media: 'print' });
await page.pdf({
path: outputPath,
format: 'A4',
preferCSSPageSize: true,
printBackground: true,
margin: {
top: '24mm',
right: '14mm',
bottom: '22mm',
left: '14mm',
},
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:9px; width:100%; padding:0 14mm;"><span class="title"></span></div>',
footerTemplate: '<div style="font-size:9px; width:100%; padding:0 14mm; text-align:right;">Page <span class="pageNumber"></span> / <span class="totalPages"></span></div>',
});
} finally {
await browser.close();
}
}
createPdf('https://example.com/report', 'report.pdf').catch((error) => {
console.error(error);
process.exitCode = 1;
});
9. Reliability and performance practices
- Reuse a browser process: launch Chromium once for a batch and create a fresh page or context per document.
- Set explicit timeouts: navigation, readiness selectors, and PDF generation should not wait forever.
- Use deterministic data: freeze report timestamps and mock unstable API responses when visual consistency matters.
- Control fonts: wait for
document.fonts.readybefore capture if typography affects pagination. - Limit concurrency: several large PDFs can consume substantial CPU and memory. Queue jobs rather than launching unlimited browsers.
- Retain diagnostics: record the URL, browser version, options, and failure category for reproducibility.
- Retry selectively: retry transient navigation or network failures, but do not blindly retry invalid URLs or authentication failures.
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(250); // only for a known short animation or layout settle
PDF cost in a self-hosted setup is driven by browser CPU, memory, storage, and any external rendering infrastructure. Large pages, many images, complex SVG, and high concurrency increase render time. Keep assets local or cacheable where possible, and avoid loading interactive code that the PDF does not need.
10. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF uses the wrong layout | Print media is active by default. | Call page.emulateMedia({ media: 'screen' }) for a screen-style PDF, or add print CSS intentionally. |
| Backgrounds are missing | Background graphics are disabled. | Set printBackground: true. |
| Brand colors look different | Print color adjustment changed them. | Use -webkit-print-color-adjust: exact and inspect the output. |
| Content is clipped or scaled unexpectedly | Format, dimensions, margins, or scale conflict. | Remove conflicting options, set explicit margins, and use preferCSSPageSize when CSS owns the page size. |
| Images or charts are absent | Lazy loading or asynchronous rendering had not finished. | Scroll to trigger lazy content and wait for a selector, application flag, or chart element. |
| Header or footer is blank | Template relies on page styles or scripts. | Use inline styles and the documented classes such as pageNumber and totalPages. |
| Navigation times out | The page or a dependency is slow, blocked, or unavailable. | Increase the timeout only when justified, inspect request failures, and provide a reliable readiness condition. |
| Browser executable is missing | Playwright browsers were not installed. | Run npx playwright install chromium in the deployment environment. |
| Different page counts across runs | Dynamic data, fonts, animations, or responsive dimensions vary. | Fix viewport and data, wait for fonts, disable animations, and use deterministic assets. |
11. Or skip the browser setup
If you need a screenshot or PDF endpoint without maintaining Playwright workers, ScreenshotNeo provides a website capture API. Its PDF options cover paper size, margins, landscape mode, and page ranges. It also supports custom CSS and JavaScript, waiting for a selector, delay, or network idle, custom headers and cookies, and bulk capture.
One GET request returns the rendered result. See the ScreenshotNeo API documentation for request options and response details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result through X-Page-Verdict and X-Billed headers. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to start.
12. Playwright PDF FAQ
Does Playwright generate PDFs in every browser engine?
Playwright supports Chromium, WebKit, and Firefox automation, but the page.pdf() workflow is documented for the Page API in the browser environment you use. Verify the behavior against your installed Playwright version and target engine.
What is the difference between format and @page?
format selects a named paper size through the API. CSS @page defines print dimensions in your stylesheet. Set preferCSSPageSize: true when the stylesheet should take precedence.
Can I return a PDF from an Express route?
Yes. Omit path, await the returned buffer, set Content-Type: application/pdf, and send the buffer from your route.
Why does a PDF have no background image?
Background graphics are opt-in for PDF generation. Add printBackground: true and confirm the asset has loaded before capture.
How do I create a landscape PDF?
Pass landscape: true with a named format, or provide custom width and height values in the desired orientation.
13. Practical checklist
- Install Playwright and the browser binary in the runtime environment.
- Choose print or screen media deliberately.
- Wait for navigation, application data, lazy assets, charts, and fonts.
- Set paper format or dimensions, margins, and scale explicitly.
- Use
preferCSSPageSizewhen CSS@pageowns the layout. - Enable
printBackgroundwhen colors or images are part of the document. - Add print color adjustment when exact colors matter.
- Reserve margins for header and footer templates.
- Use page breaks and page ranges for long documents.
- Queue concurrent jobs and capture enough diagnostics to reproduce failures.
With these choices made explicitly, page.pdf() can produce repeatable reports, invoices, documentation, and archives instead of a browser snapshot whose pagination changes from run to run.


