How to convert HTML to PDF
Updated 2026-08-30 · 7 min read
Short answer: For a one-off, press Ctrl+P and set the destination to "Save as PDF", the browser renders real text and clickable links. For repeatable output, control pagination with @page and break-inside CSS, and drive headless Chrome or Puppeteer so every run produces an identical file.
Your browser already contains an excellent HTML-to-PDF engine. The question is only how much control you need over the result.
The one-off: print to PDF
- Load the page and scroll to the bottom Lazy-loaded images below the fold never downloaded. Scroll all the way down first or they will be blank in the PDF.
- Open the print dialog Ctrl+P on Windows and Linux, Cmd+P on macOS.
- Set the destination Choose Save as PDF in the Destination dropdown, not a physical printer.
- Open More settings Set Paper size (A4 or Letter), Margins (Default, or None for a full-bleed capture), and Scale. Scale at 80 percent fits noticeably more per page and stays readable.
- Decide about backgrounds Tick Background graphics to keep coloured panels, table stripes and CSS background images. Leave it off to save ink if you will also print on paper.
- Turn off headers and footers Otherwise the browser stamps the page title, URL, date and page number onto every sheet.
The result is a real text PDF: selectable, searchable and with working links. That matters, an image-based capture of the same page looks identical and is useless for search. If the page is full of ads and navigation, clean it first with the web page printer and see printing a web page without ads.
Convert an HTML file or a URL to PDF in your browser.
Controlling the pagination with CSS
If you own the HTML, a handful of CSS rules fix the classic problems: a heading stranded at the bottom of a page, a table row split down the middle, an invoice total on a page by itself.
@page {
size: A4; /* or: A4 landscape, Letter, 210mm 297mm */
margin: 20mm 15mm;
}
@media print {
/* Never split these across a page boundary */
table, figure, blockquote, .invoice-total { break-inside: avoid; }
tr, li { break-inside: avoid; }
/* Keep a heading with the text that follows it */
h1, h2, h3 { break-after: avoid; }
/* Start each chapter on a fresh page */
.chapter { break-before: page; }
/* No orphaned single lines */
p { orphans: 3; widows: 3; }
/* Repeat table headers on every page */
thead { display: table-header-group; }
/* Print the destination of every link */
a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 90%; }
nav, aside, .no-print { display: none; }
}
Two of those rules do most of the work. break-inside: avoid on tables, figures and totals stops the single most common complaint, a block cut in half by a page boundary. thead { display: table-header-group } makes a long table repeat its column headings on every page, which is the paper equivalent of a frozen row. Everything else is a refinement you add once those two are in place.
Note: break-inside, break-before and break-after are the current CSS Fragmentation properties. The older page-break-inside family still works everywhere and is what most existing stylesheets use. Chrome and Firefox support both; keeping the legacy pair alongside the modern one costs nothing.
Watch out: position: fixed and position: sticky elements are unreliable in print, a sticky header can appear on page one only, or on every page, or overlap the content. Set them to position: static inside your print media query.
Headless Chrome for developers
For automated invoices, reports or receipts, drive the same rendering engine from the command line so every run is identical.
# Chrome / Chromium, no browser window
chrome --headless=new --disable-gpu \
--print-to-pdf=invoice.pdf \
--no-pdf-header-footer \
https://example.com/invoice/1042
# Puppeteer, with backgrounds and explicit margins
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle0" });
await page.pdf({
path: "invoice.pdf",
format: "A4",
printBackground: true,
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" }
});
await browser.close();
Two options matter more than the rest. --no-pdf-header-footer removes the URL and date stamp Chrome otherwise prints on every sheet, and waitUntil: "networkidle0" holds the capture until the page has stopped fetching, which is what prevents half-rendered charts. Always set the page format explicitly as well: Chrome defaults to US Letter regardless of your locale.
| Approach | Fidelity | Automatable | Notes |
|---|---|---|---|
| Browser Ctrl+P | Exactly what Chrome renders | No | Free, instant, keeps links and text |
| Browser tool in-page | Very high | No | No upload, good for local .html files |
| Headless Chrome / Puppeteer | Identical to Chrome | Yes | Needs a Chromium binary, ~150 MB |
| Playwright | Identical to Chromium | Yes | Cross-browser, PDF output is Chromium only |
| WeasyPrint (Python) | Good, no JavaScript | Yes | Excellent Paged Media support, lightweight |
| wkhtmltopdf | Dated engine | Yes | Archived project, old WebKit, avoid for new work |
HTML to PDF approaches compared.
Tip: Debug a print stylesheet without printing anything. In Chrome DevTools press Ctrl+Shift+P, type "Show Rendering", and set Emulate CSS media type to print. The live page now renders with your print rules so you can inspect and edit them normally.
Things that reliably break
- Web fonts that have not finished loading. In an automated run, wait for
document.fonts.readybefore generating the PDF or you get fallback fonts. - Lazy-loaded images. Set
loading="eager"in the print path, or scroll the page programmatically before capture. - Charts drawn in canvas. They render as bitmaps at screen resolution and look soft. Render SVG for print, or draw the canvas at 2x and scale it down in CSS.
- Viewport units.
100vhmeans the screen height, which has no meaning on paper. Usemmorcminside the print media query. - Elements with
overflow: hiddenand a fixed height. Only the visible portion prints; the rest is silently cut.
Once the PDF exists, compress it if it is heading into an email, or merge several generated pages into one document.
Frequently asked questions
How do I convert an HTML file to PDF for free?
Open the .html file in your browser with Ctrl+O, press Ctrl+P, and set the destination to "Save as PDF". The browser renders it exactly as it displays it, with selectable text and clickable links, at no cost and with no upload.
How do I stop a table breaking across pages in a PDF?
Add break-inside: avoid (and the legacy page-break-inside: avoid) to the table and to its rows inside a print media query. Add thead { display: table-header-group } so the header row repeats at the top of each page.
Why does my PDF look different from the web page?
The browser applies your print stylesheet, drops fixed positioning, and paginates a continuous layout. Emulate it in Chrome DevTools: Ctrl+Shift+P, "Show Rendering", set Emulate CSS media type to print, and fix what you see.
How do I set the page size and margins in CSS?
Use the @page rule: @page { size: A4; margin: 20mm 15mm; }. Add landscape after the size keyword for landscape. Browsers honour this in the print dialog, though the user can still override the margins manually.
What is the best HTML to PDF library?
For fidelity, Puppeteer or Playwright driving headless Chromium, because it is the same engine users see. For a lightweight server with no browser binary, WeasyPrint has the best CSS Paged Media support. Avoid wkhtmltopdf for new projects, it is archived and uses an old WebKit.