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

  1. 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.
  2. Open the print dialog Ctrl+P on Windows and Linux, Cmd+P on macOS.
  3. Set the destination Choose Save as PDF in the Destination dropdown, not a physical printer.
  4. 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.
  5. 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.
  6. 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.

ApproachFidelityAutomatableNotes
Browser Ctrl+PExactly what Chrome rendersNoFree, instant, keeps links and text
Browser tool in-pageVery highNoNo upload, good for local .html files
Headless Chrome / PuppeteerIdentical to ChromeYesNeeds a Chromium binary, ~150 MB
PlaywrightIdentical to ChromiumYesCross-browser, PDF output is Chromium only
WeasyPrint (Python)Good, no JavaScriptYesExcellent Paged Media support, lightweight
wkhtmltopdfDated engineYesArchived 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

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.

Tools

Keep reading