← Back to Blog

Convert HTML to PDF: Best Methods for Developers

Whether you are generating invoices, reports, or documentation, converting HTML to PDF is a task every developer encounters. The right method depends on your environment, fidelity requirements, and whether the conversion happens client side or server side.

The Problem: HTML Rendering is Not PDF Rendering

HTML is designed to reflow and adapt to screen width. PDF is a fixed-layout format with absolute coordinates. This fundamental mismatch is why HTML-to-PDF conversion is harder than it looks:

  • Page breaks: HTML has no concept of pages. PDFs are paginated. Content that looks fine on screen may split awkwardly across pages.
  • CSS support: Not every CSS property is supported in PDF rendering engines. Flexbox, Grid, and some animations may not render correctly.
  • Fonts: Web fonts (Google Fonts, custom @font-face) must be loaded before PDF generation, or text falls back to system fonts.
  • Background colors and images: Browsers suppress backgrounds in print mode by default - you must explicitly enable them.

Method 1: Browser Print Dialog (Ctrl+P / window.print)

The simplest approach: use the browser's built-in print functionality and choose "Save as PDF" as the printer.

// Trigger the print dialog programmatically
window.print();

This uses the browser's rendering engine directly, giving you the best CSS fidelity. The limitation is user interaction - the print dialog appears and the user must choose settings.

To control the print layout, use CSS print media queries:

@media print {
  /* Hide navigation, footers, buttons */
  nav, footer, .no-print { display: none; }

  /* Force backgrounds to print */
  * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }

  /* Control page breaks */
  .page-break { page-break-before: always; }
  .keep-together { page-break-inside: avoid; }

  /* Set page size and margins */
  @page {
    size: A4;
    margin: 20mm 15mm;
  }

  /* Ensure full-width layout */
  body { width: 100%; margin: 0; }
}

Best for: Simple documents, user-initiated PDFs, cases where fidelity matches the browser view.

Method 2: Puppeteer (Node.js, Headless Chrome)

Puppeteer controls a headless Chrome instance and is the gold standard for server side HTML-to-PDF generation. It renders your HTML exactly as Chrome would, including JavaScript execution, fonts, and CSS animations.

npm install puppeteer
const puppeteer = require('puppeteer');

async function htmlToPdf(htmlContent, outputPath) {
  const browser = await puppeteer.launch({
    headless: 'new', // use new headless mode
    args: ['--no-sandbox', '--disable-setuid-sandbox'] // required in Docker/CI
  });

  const page = await browser.newPage();

  // Option A: render from HTML string
  await page.setContent(htmlContent, {
    waitUntil: 'networkidle0' // wait for all network requests to finish (fonts etc)
  });

  // Option B: render from URL
  // await page.goto('https://example.com/report', { waitUntil: 'networkidle0' });

  const pdf = await page.pdf({
    path: outputPath,       // omit to return buffer instead of writing file
    format: 'A4',
    margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
    printBackground: true,  // include background colors and images
    displayHeaderFooter: true,
    headerTemplate: '
My Report
', footerTemplate: '
of
' }); await browser.close(); return pdf; // Buffer if no path specified }

Best for: Invoices, reports, server side rendering, complex CSS layouts, documents that include JavaScript-rendered content.

Puppeteer's waitUntil: 'networkidle0' waits until there are no more than 0 network connections for 500ms. This ensures web fonts and external stylesheets are loaded before generating the PDF.

Method 3: Playwright (Node.js / Python / .NET)

Playwright is a more modern alternative to Puppeteer with support for Chromium, Firefox, and WebKit. Its PDF API is nearly identical:

npm install playwright
const { chromium } = require('playwright');

async function htmlToPdf(url) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto(url, { waitUntil: 'networkidle' });

  const pdfBuffer = await page.pdf({
    format: 'A4',
    margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
    printBackground: true
  });

  await browser.close();
  return pdfBuffer;
}

Best for: The same use cases as Puppeteer. Choose Playwright if you need cross-browser support or are already using it for testing.

Convert HTML to PDF Instantly - Free Online Tool

Paste your HTML and download a PDF in seconds. No installation, no upload to servers - runs entirely in your browser.

Open HTML to PDF Tool

Method 4: wkhtmltopdf (C++ Webkit Engine)

wkhtmltopdf is a command-line tool based on the Qt WebKit rendering engine. It is older than Puppeteer and does not support modern CSS as well, but it is fast and has no dependency on Chrome:

# Install (Ubuntu/Debian)
sudo apt-get install wkhtmltopdf

# Basic conversion
wkhtmltopdf input.html output.pdf

# From URL with options
wkhtmltopdf --page-size A4 \
  --margin-top 20mm \
  --margin-bottom 20mm \
  --print-media-type \
  https://example.com/report output.pdf

From Node.js, use the wkhtmltopdf npm package:

const wkhtmltopdf = require('wkhtmltopdf');
const fs = require('fs');

wkhtmltopdf('<h1>Hello World</h1><p>My PDF content</p>', { pageSize: 'A4' })
  .pipe(fs.createWriteStream('output.pdf'));

Best for: Server environments where installing Chrome is not feasible. Note: wkhtmltopdf uses an old WebKit version and does not support Flexbox well.

Method 5: CSS @page and Print Stylesheets

For the best printed output without external dependencies, invest in a proper print stylesheet. The CSS @page rule and print-specific properties give you precise control:

@page {
  size: A4 portrait;
  margin: 25mm 20mm 25mm 20mm;

  @top-center {
    content: "Company Report 2026";
    font-size: 9pt;
    color: #666;
  }

  @bottom-right {
    content: "Page " counter(page) " of " counter(pages);
    font-size: 9pt;
  }
}

/* Avoid orphans and widows */
p { orphans: 3; widows: 3; }

/* Keep headings with following content */
h1, h2, h3 { page-break-after: avoid; }

/* Force page break before major sections */
.new-section { page-break-before: always; }

/* Prevent tables from splitting across pages */
table { page-break-inside: avoid; }

/* Show full URLs in print */
a[href]::after { content: " (" attr(href) ")"; }

/* Hide interactive elements */
button, input, .nav, .sidebar { display: none; }

Best for: Content-heavy sites where users print directly, reducing dependency on server side libraries.

Method 6: jsPDF and html2canvas (Client-Side)

For fully client side PDF generation without a server, html2canvas renders the DOM to a canvas, and jsPDF embeds it as an image in a PDF:

npm install jspdf html2canvas
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';

async function exportToPdf(elementId) {
  const element = document.getElementById(elementId);

  const canvas = await html2canvas(element, {
    scale: 2,           // 2x resolution for retina quality
    useCORS: true,      // allow cross origin images
    logging: false
  });

  const imgData = canvas.toDataURL('image/png');
  const pdf = new jsPDF({
    orientation: 'portrait',
    unit: 'mm',
    format: 'a4'
  });

  const imgWidth = 210; // A4 width in mm
  const imgHeight = (canvas.height * imgWidth) / canvas.width;

  pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
  pdf.save('document.pdf');
}

Limitation: The PDF contains a rasterized image, not real text. Text in the PDF will not be searchable or copy-pasteable.

Best for: Quick client side exports where text searchability is not required.

Step-by-Step: Generate an Invoice PDF with Puppeteer

  1. Create your HTML template with inline CSS or a link to a stylesheet that Puppeteer can load
  2. Populate data using a template engine like Handlebars, Mustache, or simple string interpolation
  3. Launch Puppeteer and call page.setContent(html, { waitUntil: 'networkidle0' })
  4. Call page.pdf() with your format and margin settings. Use printBackground: true
  5. Return the buffer as a download response or write to disk
  6. Close the browser with browser.close()
// Express.js endpoint that returns a PDF download
app.get('/invoice/:id', async (req, res) => {
  const invoice = await db.getInvoice(req.params.id);
  const html = renderInvoiceTemplate(invoice); // your template function

  const pdfBuffer = await htmlToPdf(html); // function from Method 2 above

  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.id}.pdf"`);
  res.send(pdfBuffer);
});

Comparison: Which Method to Use

  • Best CSS fidelity, server side: Puppeteer or Playwright
  • No Chrome dependency: wkhtmltopdf (older CSS support)
  • Client side, no server: jsPDF + html2canvas (image-based) or window.print()
  • Print optimization without libraries: CSS @page + print stylesheets
  • Quick online conversion: SecureBin HTML to PDF tool

FAQ

Why are my web fonts missing in the generated PDF?

Puppeteer needs to download web fonts before rendering. Use waitUntil: 'networkidle0' when calling page.setContent() or page.goto(). Alternatively, use Base64-encoded fonts embedded in your CSS using @font-face with a src: url('data:font/woff2;base64,...') to guarantee font availability without network requests.

How do I add page numbers to the PDF?

In Puppeteer, set displayHeaderFooter: true and provide HTML templates for headerTemplate and footerTemplate. Use <span class="pageNumber"></span> and <span class="totalPages"></span> as placeholders that Puppeteer fills automatically.

Why are background colors and images not showing in my PDF?

Both browsers and Puppeteer suppress background graphics by default in print mode to save ink. In Puppeteer, set printBackground: true in the page.pdf() options. In CSS print stylesheets, add -webkit-print-color-adjust: exact; print-color-adjust: exact; to the relevant elements.

Can I generate PDFs from a URL that requires authentication?

Yes. With Puppeteer, you can call page.setCookie() or page.setExtraHTTPHeaders() before navigating to the URL. This lets you pass session cookies or Bearer tokens to access authenticated pages.

How do I handle page breaks so tables and sections do not split?

Use CSS properties: page-break-inside: avoid on the element you want to keep together, page-break-before: always to force a new page before an element, and page-break-after: avoid to keep headings attached to following content. The modern equivalents are break-inside, break-before, and break-after.

Use our free tool here → HTML to PDF Converter

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.