Converting HTML to PDF is a task developers and non-developers face constantly: invoices, reports, certificates, documentation snapshots. The conversion method you pick determines output quality, file size, and whether the text stays selectable or gets flattened into an image.
This guide covers every method from browser printing to production REST APIs, with code examples and a comparison table.
The fastest method: free online converter
The fastest way to convert HTML to PDF is PDF4.dev's HTML-to-PDF tool. Paste or upload your HTML, click Convert, and download the PDF. No account, no watermark, no file upload to a third-party server.
The tool uses headless Chromium server-side, so the output is pixel-accurate: the same CSS rendering engine as Google Chrome, including support for flexbox, grid, custom fonts, and modern CSS.
What it supports:
- Raw HTML strings pasted directly
- External CSS loaded via
<link>tags (as long as the URLs are public) - Images via absolute URLs or base64 data URIs
- Custom
@pagerules for page size and margins
What it doesn't support:
- JavaScript-heavy Single Page Apps that require client-side rendering
- Forms or interactive elements (PDFs are static)
Method 2: browser print dialog (Ctrl+P)
Every modern browser can save any webpage as a PDF. Press Ctrl+P on Windows/Linux or Cmd+P on macOS, change the destination to Save as PDF, and click Save.
This is the right tool for saving a live webpage you're viewing. It's not reliable for automating PDF generation, because:
- Output varies between Chrome, Firefox, and Safari
- Backgrounds and colors are sometimes stripped unless you check "Background graphics"
- You can't control margins precisely without CSS
@pagerules on the source page - There's no API to call programmatically
For one-off manual saves, Ctrl+P is fine. For anything repeatable, use a dedicated tool or API.
CSS rules that control PDF output
When you convert HTML to PDF, @media print rules control how the content is laid out on the page. The PDF renderer treats the page as a printed medium, so these rules apply directly.
/* Set page format and margins */
@page {
size: A4;
margin: 20mm;
}
/* Hide elements that shouldn't appear in the PDF */
.navbar,
.footer,
.cookie-banner {
display: none;
}
/* Force a page break before section headings */
h2 {
page-break-before: always;
}
/* Prevent a table row from splitting across pages */
tr {
page-break-inside: avoid;
}For a complete reference on CSS print rules, including page break control, headers/footers, and widow/orphan handling, see CSS print styles for PDF generation.
Method 3: REST API (recommended for production)
For automated, repeatable PDF generation, the PDF4.dev REST API is the most direct approach. Send a POST request with your HTML and receive a PDF binary in response.
const response = await fetch("https://pdf4.dev/api/v1/render", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
html: `<html>
<head>
<style>
body { font-family: Inter, sans-serif; padding: 40px; }
h1 { color: #111827; }
</style>
</head>
<body>
<h1>Invoice #001</h1>
<p>Amount due: $1,500.00</p>
</body>
</html>`,
format: {
preset: "a4",
margins: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
},
}),
});
const pdf = await response.arrayBuffer();
// write to disk, stream to client, or upload to S3The API returns a binary application/pdf response. The response includes X-Duration-Ms and X-File-Size headers so you can monitor render performance.
Method 4: Playwright directly (self-hosted)
Playwright is an open-source browser automation library from Microsoft. Its page.pdf() method converts any HTML or URL to a PDF using headless Chromium. This is the same engine PDF4.dev uses internally.
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(`
<html>
<body>
<h1>Invoice #001</h1>
<p>Amount due: $1,500.00</p>
</body>
</html>
`, { waitUntil: "load" });
const pdf = await page.pdf({
format: "A4",
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
printBackground: true,
});
await browser.close();When Playwright makes sense:
- You already have Playwright in your test suite
- You need to render pages that require client-side JavaScript
- You want full control over the browser instance
When Playwright becomes a burden in production:
Playwright adds ~280MB to your Docker image from the Chromium binary. A cold start on AWS Lambda or Vercel takes 3-5 seconds to spin up the browser. Concurrent requests need separate browser instances unless you manage a warm pool. Memory leaks in long-lived instances require monitoring and restarts.
If you're running fewer than a few hundred PDFs per day and you control your server, this is manageable. At scale, or on serverless infrastructure, the overhead matters.
PDF4.dev runs the same Playwright/Chromium pipeline but manages the browser pool for you. The API call takes ~150ms on a warm instance, with no Docker overhead on your side.
Method 5: html2canvas + jsPDF (client-side)
Some use cases require generating PDFs entirely in the browser, without a server. The common approach is html2canvas + jsPDF: render the HTML element to a canvas, then embed the canvas as an image in a PDF.
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
const element = document.getElementById("content");
const canvas = await html2canvas(element, { scale: 2 });
const imgData = canvas.toDataURL("image/png");
const pdf = new jsPDF({ format: "a4", unit: "mm" });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight);
pdf.save("document.pdf");What this gets you: fully client-side, no server, no API key.
What you give up: the output is a rasterized image, not real text. Characters are not selectable or searchable. File sizes are large (a one-page document can exceed 1MB). CSS support is partial — gradients, shadows, and complex layouts often don't render correctly.
Use this only when you need a "screenshot to PDF" of a styled DOM element and text searchability doesn't matter.
HTML-to-PDF method comparison
| Method | Quality | Text selectable | Speed | Infrastructure | Cost |
|---|---|---|---|---|---|
| PDF4.dev online tool | High (Chromium) | Yes | Instant | None | Free |
| Browser Ctrl+P | High | Yes | Manual | None | Free |
| PDF4.dev API | High (Chromium) | Yes | ~150ms warm | None | Free tier |
| Playwright (self-hosted) | High (Chromium) | Yes | ~300ms warm | Chromium (~280MB) | Server cost |
| html2canvas + jsPDF | Medium (rasterized) | No | Fast | None | Free |
| WeasyPrint (Python) | Medium (CSS 2.1) | Yes | ~500ms | Python runtime | Free/OSS |
| wkhtmltopdf | Low (deprecated Qt) | Yes | ~400ms | Qt WebKit | Free/OSS |
Render times are approximate benchmarks with a warm instance on standard VPS hardware. Cold start times for Playwright are 3-5x higher on serverless platforms.
Working with dynamic data
The most common production use case isn't converting a static HTML file: it's generating a PDF with different data each time, for a different invoice, user, or report.
PDF4.dev templates use Handlebars syntax. You write the HTML once with {{variable}} placeholders, then pass different data objects per render call.
// Create the template once (or use the dashboard)
const createResponse = await fetch("https://pdf4.dev/api/v1/templates", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Invoice",
html: `<html>
<body>
<h1>Invoice {{invoice_number}}</h1>
<p>Customer: {{customer_name}}</p>
<p>Amount: {{amount}}</p>
{{#if paid}}
<span class="badge">PAID</span>
{{/if}}
</body>
</html>`,
}),
});
const { id: templateId } = await createResponse.json();
// Then render with different data for each invoice
const pdfResponse = await fetch("https://pdf4.dev/api/v1/render", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
template_id: templateId,
data: {
invoice_number: "INV-2026-001",
customer_name: "Acme Corp",
amount: "$4,200.00",
paid: true,
},
}),
});You can also pass raw HTML with variables in the same call, without creating a stored template:
{
"html": "<p>Hello {{name}}, your order {{order_id}} is ready.</p>",
"data": { "name": "Alice", "order_id": "ORD-789" }
}Common problems and fixes
Images not loading
Images with relative paths (src="./logo.png") don't resolve when the HTML is rendered from a string, because there's no base URL. Use absolute URLs (https://yourdomain.com/logo.png) or base64 data URIs instead.
<!-- ❌ Won't load from an HTML string -->
<img src="./logo.png" />
<!-- ✅ Works from anywhere -->
<img src="https://cdn.example.com/logo.png" />
<!-- ✅ Also works: base64 data URI -->
<img src="data:image/png;base64,iVBORw0KGgo..." />Fonts rendering as system fallbacks
If your HTML references a Google Font via <link>, it must be loaded before rendering starts. In Playwright, set waitUntil: "networkidle" instead of "load". In the PDF4.dev API, pass the google_fonts_url field in the format object — the renderer fetches and embeds the font before generating the PDF.
{
"html": "...",
"format": {
"preset": "a4",
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Inter&display=swap",
"font_family": "Inter, sans-serif"
}
}Content cut off at page boundaries
Tables rows split across pages by default. Add page-break-inside: avoid to tr elements, or use the newer break-inside: avoid. For paragraphs, add orphans: 3; widows: 3 to prevent single-line remnants at the top or bottom of a page.
Background colors not printing
By default, Chromium suppresses background colors and images in print mode to save printer ink. In Playwright, pass printBackground: true to page.pdf(). The PDF4.dev API enables this automatically.
PDF format options
The PDF4.dev API accepts a format object to control the output:
| Field | Type | Example | Description |
|---|---|---|---|
preset | string | "a4" | Page size: a4, a4-landscape, letter, letter-landscape, square, custom |
margins | object | { top: "20mm" } | Margins in CSS units (mm, px, cm, in) |
background_color | string | "#ffffff" | Page background color |
font_family | string | "Inter, sans-serif" | Default font for the page |
font_size | string | "14px" | Base font size |
google_fonts_url | string | "https://fonts.google..." | Loads and embeds the font before rendering |
width / height | string | "210mm" | For preset: "custom" dimensions |
From HTML to PDF in production: what to watch
Template version control. Store your HTML templates in your codebase or in the PDF4.dev dashboard, not hardcoded in application logic. When the template changes, it should be a reviewed commit, not a string buried in a request handler.
Error handling. The PDF4.dev API returns structured JSON errors on failure: { error: { type, code, message } }. Handle 4xx responses explicitly — a missing template_id or malformed HTML returns a clear error code, not a blank PDF.
PDF size. A typical A4 invoice HTML converts to 80-200KB. Embedding large raster images (PNG screenshots, unoptimized photos) can push this to several MB. Compress images before embedding, or use SVG for icons and charts.
Logging. The PDF4.dev dashboard logs every render call with template, status, duration, and file size. Use this to catch slowdowns and track usage against your quota.
For a deeper look at HTML-to-PDF in a Node.js application, including error handling and streaming to the client, see Generate PDF from HTML in Node.js. For building invoice templates specifically, see How to generate PDF invoices programmatically.
Summary
HTML-to-PDF conversion covers a range from one-off browser saves to high-volume API generation. The right choice depends on your use case:
- One-off manual save: browser Ctrl+P or PDF4.dev's free tool
- Automated production generation: PDF4.dev REST API (no infrastructure to manage)
- Self-hosted, custom pipeline: Playwright with a warm browser pool
- Browser-only, no server: html2canvas + jsPDF (image-based, lower quality)
The HTML-to-PDF tool is free, requires no account, and handles most conversion tasks in under five seconds.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.