ToolSink
Back to blog

HTML to PDF Conversion: How It Works and How to Automate It

ToolSink Team

Most business-generated PDFs you receive — invoices, receipts, exported reports — didn't start as PDFs at all. They started as an HTML template, styled with CSS, and were converted to PDF at the moment of generation. Understanding how that conversion actually works is useful both for using a converter tool correctly and for building the process yourself if you're generating documents programmatically.

What's actually running: headless browsers, not magic

An HTML-to-PDF converter is, under the hood, usually a headless browser — a real browser engine (Chromium, in most modern tools) running without a visible window. It loads the HTML, applies CSS, executes any JavaScript needed to render dynamic content, then captures the rendered layout and paginates it into a PDF instead of displaying it on screen.

The most common tools implementing this:

  • Puppeteer / Playwright (Node.js libraries controlling headless Chromium) — the standard choice for programmatic PDF generation today
  • wkhtmltopdf — older, uses a WebKit rendering engine; still widely used but has known limitations with modern CSS (flexbox/grid support is inconsistent)
  • Prince — a commercial tool built specifically around print-quality CSS support (@page rules, running headers/footers)

Because it's a real rendering engine, the output should match what you'd see in a browser — with one major caveat: print styles.

For the specific CSS issues that cause an HTML-to-PDF export to look different from the live page — @media print overrides, fixed-position elements breaking across page boundaries, and how to prevent content from splitting mid-element — see PDF Merging, Splitting, and Compression: How They Actually Work, which covers this in detail.

A real example: generating an invoice PDF

Here's the actual shape of automated invoice generation using Puppeteer:

const puppeteer = require('puppeteer');

async function generateInvoicePDF(invoiceHtml, outputPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Load the invoice HTML (already rendered with customer/order data)
  await page.setContent(invoiceHtml, { waitUntil: 'networkidle0' });

  await page.pdf({
    path: outputPath,
    format: 'A4',
    printBackground: true, // without this, background colors/images are stripped
    margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
    displayHeaderFooter: true,
    footerTemplate: '<div style="font-size:10px; width:100%; text-align:center;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
    headerTemplate: '<div></div>', // empty header, still required if footer is enabled
  });

  await browser.close();
}

Two details that matter for correctness, not just style:

  • printBackground: true is easy to miss and causes invoices to render with a stark white background even if your template has branded colors — because browsers don't print backgrounds by default.
  • waitUntil: 'networkidle0' matters if your template loads external images or fonts — without waiting for network activity to settle, you can end up with a PDF missing a logo that hadn't finished loading yet.

Where this approach actually gets used

  • Automated invoicing — generate a PDF the moment an order completes, using the same HTML template for every customer with just the data swapped in
  • Exporting dashboards/reports — turning an interactive HTML report into a static, shareable snapshot for a meeting or stakeholder who doesn't have dashboard access
  • Archiving web pages — creating a permanent, unalterable record of a page's content at a specific point in time (useful for compliance, terms-of-service versions, or dispute records) — a bookmark doesn't survive a site redesign; a PDF does
  • Saving articles for offline reading — personal use, but the same mechanism

Where it's the wrong tool

If you need the resulting document to remain editable text (not flattened print output), or if the source content changes frequently and you'd be re-generating constantly, a live document format or a database export is usually a better fit than repeatedly converting HTML snapshots. HTML-to-PDF is for a document meant to be a fixed, point-in-time record — not a live one.

Quick conversion without setting up tooling

For a one-off conversion rather than an automated pipeline, ToolSink's HTML to PDF tool handles the same headless-rendering process directly in your browser for pasted HTML or a page URL.