HTML to PDF: the developer's complete guide
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
- Use
printBackground: true— backgrounds are dropped by default and white-on-white text is the #1 support ticket. - Wrap rows in
page-break-inside: avoidso tables don't split mid-row. - Use mm-based margins and
page-break-after: alwaysfor deterministic multi-page layouts.
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.