← Blog

HTML to PDF: the developer's complete guide

September 7, 2026 · 7 min read

Invoices, tickets, reports, shipping labels — sooner or later, almost every web app has to produce a PDF. And the fastest way to build one is usually to not build it at all: write HTML and CSS you already know, then convert. Here are the main routes, and where each one breaks.

1. window.print() + print stylesheets

The zero-dependency option: style a print-specific view with @media print, and let the user print to PDF. Fine for a single, human-driven workflow. No automation, inconsistent output across browsers, and the PDF never touches your server.

2. wkhtmltopdf and friends

The classic CLI tools. Still everywhere in legacy systems, but built on an ancient WebKit: no modern CSS grid or flexbox fidelity, no JavaScript, and security patches are sporadic. Starting a new project on it in 2026 is a decision you'll revisit painfully.

3. Headless Chrome (Puppeteer / Playwright)

The gold standard for fidelity — it's literally Chrome printing the page. You get modern CSS, web fonts, JavaScript-rendered content, and control over headers, footers, page size and margins:

const page = await browser.newPage();
await page.goto(invoiceUrl, { waitUntil: "networkidle" });
await page.pdf({
  format: "A4",
  printBackground: true,
  margin: { top: "12mm", bottom: "12mm" },
});

The cost isn't the code — it's running Chrome in production: version drift, memory spikes, font packages on servers, and concurrency planning.

4. A PDF rendering API

Same Chrome-quality output, none of the ops. With OtterSnap the whole integration is:

import { OtterSnap } from "ottersnap";

const otter = new OtterSnap(process.env.OTTERSNAP_KEY);

const pdf = await otter.pdf(
  "https://yourapp.com/invoices/1042",
  { paper: "A4" }
);

Your invoice page is just HTML with a print stylesheet — OtterSnap handles the browser, the fonts and the failures.

Print-CSS gotchas worth knowing

The bottom line

One-off, human-driven → print stylesheet. New automated build → headless Chrome you manage, or an API that manages it for you from $0/month.