Puppeteer turns HTML into a PDF with page.pdf(): Chrome prints the page with its print engine, so you get real text, vector graphics, CSS page breaks and running headers and footers. The options that matter are format (or width and height), margin, printBackground, displayHeaderFooter with its templates, and preferCSSPageSize; the rest of the work is print CSS. Everything below ran against Puppeteer 25.7 and Chrome, and the PDFOptions reference lists the full set.
The minimal version
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'load' });
// or: await page.goto('https://example.com/invoice/114', { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
});
await browser.close();
// pdf is a Uint8Array; write it with fs.writeFile or send it in a response.
Three defaults catch people out:
formatdefaults toletter. In Europe that is the wrong paper; setA4(orwidthandheight).- No backgrounds without
printBackground: true. A coloured header comes out white. Puppeteer also notes that colours are adjusted for printing by default — add-webkit-print-color-adjust: exactto elements whose colours must stay exact (page.pdf()). - Print media. The page is rendered with
@media printstyles. If your page hides things in print, or you want the screen look, callpage.emulateMediaType('screen')first.
page.pdf() waits for document.fonts.ready by default (waitForFonts: true), so web fonts loaded by the page are in the file; what it cannot wait for is data your page fetches after load — use waitUntil: 'networkidle0' with goto, or wait for a selector that only appears when the content is there.
Paper size: options or CSS
You can set the page size in the call (format: 'A4', or width: '100mm', height: '150mm' for a label) or in CSS with @page { size: 100mm 150mm; margin: 0 }. The CSS size is used only with preferCSSPageSize: true; without it, format wins — in our test the same page came out 100×150 mm with the flag and 210×297 mm (A4) without. Keeping the size in CSS means the template carries its own paper, which is handy when one service prints several document types.
landscape: true turns the paper; scale (0.1 to 2) shrinks or enlarges the rendering — a quick fix for a table that is a little too wide, at the cost of smaller text.
Page breaks
Puppeteer has no page-break option of its own; Chrome follows CSS fragmentation rules:
h2.chapter { break-before: page; } /* always start a new page */
tr, .card, figure { break-inside: avoid; } /* never split these */
thead { display: table-header-group; } /* repeat on every page */
tfoot { display: table-footer-group; }
In a 90-row invoice table, thead repeated at the top of each of the three pages the table ran onto, and the "Terms" section with break-before: page started on its own page. If a block is taller than a page, break-inside: avoid cannot be honoured and it splits anyway.
Headers, footers and page numbers
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
displayHeaderFooter: true,
headerTemplate: '<span></span>',
footerTemplate: `
<div style="font-size:9px; width:100%; text-align:center; color:#666">
Invoice 2026-114 · page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>`,
});
The templates support five classes that Chrome fills in: date, title, url, pageNumber and totalPages. Things to know:
- They are separate documents. Your page's stylesheet does not apply inside them, so put the styles inline — including
font-size: in our test a footer without one came out at under 1 pt. - They live in the margin. The header and footer are drawn in the top and bottom margins, so give those margins room; with zero margins our footer printed on top of the last lines of text.
- An empty header still needs a template. With
displayHeaderFooter: trueand noheaderTemplate, Chrome prints its default header — the date and the document title. Pass'<span></span>'to hide it. - Keep them self-contained. Put a small logo in as a data URI rather than relying on the template to fetch a URL.
Running it in production
The PDF part is short; operating Chrome is the real work:
- Docker. Use an image with Chrome's dependencies and fonts installed — Puppeteer documents an official one — and give it enough shared memory (
--shm-size=1gbor--disable-dev-shm-usage). Install the fonts your documents use; a missing font silently falls back to another one. - Reuse the browser. Launching Chrome for every request costs startup time and memory each time. Keep one browser and open a page per job, with a queue in front so a burst does not start fifty tabs.
- Timeouts.
page.pdf()has a 30-second defaulttimeout; a very long document can exceed it. Settimeoutdeliberately and close the page infinally. - User content. If the HTML comes from users, the browser can fetch URLs from inside your network. Block private addresses (or run Chrome with no network and inline everything) before you print anything a stranger wrote.
Other options worth knowing
| Option | What it does |
|---|---|
pageRanges: '1-3, 5' |
Print only some pages; '1' keeps a one-page summary |
omitBackground: true |
No default white background — a PDF with transparency |
tagged (default true) |
Tagged, more accessible PDF (experimental) |
outline: true |
A bookmark outline from headings (experimental) |
path: 'out.pdf' |
Also write the file to disk |
When an API is the simpler option — and when it is not
If your documents are one page each — certificates, event tickets, badges, shipping labels, coupons, one-page receipts — you do not need to run Chrome yourself. The Shareglint API keeps the HTML and CSS as a template and returns a one-page vector PDF for each request ("format": "pdf"), the page exactly the template's size, or renders a whole CSV in one batch; 100 renders a month are free and Starter is $9 for 2,000. The PDF generation API page has the details and the certificates use case a worked example.
If your documents run over several pages — invoices with long item lists, reports, contracts — the image API is the wrong tool: it makes exactly one page per PDF, with no page breaks, repeated headers or page-number footers. Keep Puppeteer for those, or use a service built for pagination such as DocRaptor or PDFShift. For images rather than PDFs, the Puppeteer screenshot guide covers page.screenshot() the same way.