Get started

How to convert HTML to PDF: complete guide (2026)

Convert HTML to PDF online, in the browser, or via API. Free tool, browser printing, Node.js, Python, PHP, and REST API methods compared.

benoitdedMarch 22, 202611 min read

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 @page rules 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 @page rules 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.

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 S3

The 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

MethodQualityText selectableSpeedInfrastructureCost
PDF4.dev online toolHigh (Chromium)YesInstantNoneFree
Browser Ctrl+PHighYesManualNoneFree
PDF4.dev APIHigh (Chromium)Yes~150ms warmNoneFree tier
Playwright (self-hosted)High (Chromium)Yes~300ms warmChromium (~280MB)Server cost
html2canvas + jsPDFMedium (rasterized)NoFastNoneFree
WeasyPrint (Python)Medium (CSS 2.1)Yes~500msPython runtimeFree/OSS
wkhtmltopdfLow (deprecated Qt)Yes~400msQt WebKitFree/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:

FieldTypeExampleDescription
presetstring"a4"Page size: a4, a4-landscape, letter, letter-landscape, square, custom
marginsobject{ top: "20mm" }Margins in CSS units (mm, px, cm, in)
background_colorstring"#ffffff"Page background color
font_familystring"Inter, sans-serif"Default font for the page
font_sizestring"14px"Base font size
google_fonts_urlstring"https://fonts.google..."Loads and embeds the font before rendering
width / heightstring"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:

Html To PdfTry it freeMerge PdfTry it freeCompress PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.