Skip to content
Back to Blog
January 15, 2025Tom Silas Helmke12 min readTutorial

How to Convert HTML to PDF with JavaScript in 2025

Client-side PDF generation has become essential for modern web applications. Whether you're building invoice generators, report dashboards, or document exporters, this comprehensive guide covers everything you need to know about converting HTML to PDF using JavaScript.

1. Why Choose Client-Side PDF Generation?

Before diving into code, let's understand why client-side processing is often the better choice compared to server-side solutions like Puppeteer or wkhtmltopdf.

Privacy First

Documents never leave the user's device. Critical for GDPR compliance and handling sensitive data like invoices, contracts, or medical records.

Instant Processing

No network latency, no upload time, no server queue. PDFs generate in milliseconds using the client's CPU directly.

Zero Infrastructure

No servers to maintain, no Docker containers, no cloud costs. Everything runs in the browser with a simple npm package.

Perfect CSS Support

Uses the browser's native rendering engine. CSS Grid, Flexbox, and modern styles work exactly as they appear on screen.

2. The Technology Stack: html2pdf.js

The most robust library for client-side HTML to PDF conversion in 2025 is html2pdf.js. It's a high-level wrapper that combines two powerful libraries:

  • html2canvas: Renders the DOM into a Canvas element by essentially taking a "screenshot" of your HTML content.
  • jsPDF: Takes that canvas image and places it onto PDF pages, handling page breaks and document structure.

This combination provides "What You See Is What You Get" (WYSIWYG) PDF generation. Your CSS styles, fonts, and layout are preserved because html2canvas captures the exact rendered output of the browser.

3. Basic Usage & Installation

Installation

Install via npm for modern build systems:

npm install html2pdf.js

Or include via CDN for simple HTML pages:

<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>

Basic Implementation

The simplest implementation selects an HTML element and converts it to a downloadable PDF:

import html2pdf from 'html2pdf.js';

function generatePDF() {
  // Select the element you want to convert
  const element = document.getElementById('content');
  
  // Configuration options
  const options = {
    margin: 10,
    filename: 'document.pdf',
    image: { type: 'jpeg', quality: 0.98 },
    html2canvas: { scale: 2, useCORS: true },
    jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
  };
  
  // Generate and download
  html2pdf().set(options).from(element).save();
}

Let's break down the options object:

  • margin: Page margins in the unit specified (mm by default)
  • filename: The name of the downloaded file
  • image.quality: JPEG quality (0.1 to 1.0) - higher means better quality but larger files
  • html2canvas.scale: Rendering resolution multiplier (2 = ~192 DPI, good for print)
  • jsPDF.format: Paper size ('a4', 'letter', 'legal', or custom [width, height])

4. Advanced Configuration

For production applications, you'll need more control over fonts, images, and page breaks. Here's a comprehensive configuration:

import html2pdf from 'html2pdf.js';

async function generateAdvancedPDF() {
  const element = document.getElementById('report');
  
  // Wait for fonts to load
  await document.fonts.ready;
  
  // Wait for images to load
  const images = element.querySelectorAll('img');
  await Promise.all(
    Array.from(images).map(img => {
      if (img.complete) return Promise.resolve();
      return new Promise(resolve => {
        img.onload = resolve;
        img.onerror = resolve;
      });
    })
  );
  
  const options = {
    margin: [15, 15, 20, 15], // top, right, bottom, left
    filename: 'report-' + new Date().toISOString().slice(0,10) + '.pdf',
    image: { type: 'jpeg', quality: 0.95 },
    html2canvas: { 
      scale: 2,
      useCORS: true,
      logging: false,
      letterRendering: true
    },
    jsPDF: { 
      unit: 'mm', 
      format: 'a4', 
      orientation: 'portrait',
      compress: true
    },
    pagebreak: { 
      mode: ['avoid-all', 'css', 'legacy'],
      before: '.page-break-before',
      after: '.page-break-after',
      avoid: '.keep-together'
    }
  };
  
  try {
    await html2pdf().set(options).from(element).save();
    console.log('PDF generated successfully');
  } catch (error) {
    console.error('PDF generation failed:', error);
  }
}

Understanding Page Breaks

One of the trickiest aspects of PDF generation is preventing content from being split awkwardly across pages. The pagebreak option gives you control:

  • mode: 'avoid-all': Tries to avoid breaking inside any element
  • mode: 'css': Respects CSS page-break-* properties
  • before: CSS selector for elements that should start on a new page
  • after: CSS selector for elements that should be followed by a page break
  • avoid: CSS selector for elements that should never be split

Important: Font Loading

Web fonts (Google Fonts, Adobe Fonts) must be fully loaded before generating the PDF. Use document.fonts.ready to wait for font loading, or the PDF may render with fallback fonts.

5. React Integration

Integrating html2pdf.js with React requires a few considerations: using refs to access DOM elements, handling loading states, and proper error handling.

import { useRef, useState } from 'react';
import html2pdf from 'html2pdf.js';

function InvoiceComponent({ invoiceData }) {
  const invoiceRef = useRef(null);
  const [isGenerating, setIsGenerating] = useState(false);

  const downloadPDF = async () => {
    if (!invoiceRef.current) return;
    
    setIsGenerating(true);
    
    try {
      const options = {
        margin: 10,
        filename: `invoice-${invoiceData.number}.pdf`,
        image: { type: 'jpeg', quality: 0.98 },
        html2canvas: { scale: 2 },
        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
      };
      
      await html2pdf().set(options).from(invoiceRef.current).save();
    } catch (error) {
      console.error('Failed to generate PDF:', error);
    } finally {
      setIsGenerating(false);
    }
  };

  return (
    <div>
      <div ref={invoiceRef} className="invoice-container">
        {/* Your invoice content here */}
        <h1>Invoice #{invoiceData.number}</h1>
        <p>Date: {invoiceData.date}</p>
        {/* ... more invoice content */}
      </div>
      
      <button onClick={downloadPDF} disabled={isGenerating}>
        {isGenerating ? 'Generating...' : 'Download PDF'}
      </button>
    </div>
  );
}

React Best Practices

  • • Use useRef instead of document.getElementById
  • • Add loading states to prevent double-clicks during generation
  • • Wrap in try/catch for error handling
  • • Consider using a portal for print-only content that shouldn't be visible

7. Troubleshooting Common Issues

Problem: Blank or White PDF

Cause: The target element isn't visible when html2canvas captures it.

Solution: Ensure the element has display: block (not none) and is within the viewport. If using modals or hidden content, temporarily show it before generation.

Problem: Images Not Appearing

Cause: Cross-origin images (from different domains) or lazy-loaded images that haven't loaded yet.

Solution: Add useCORS: true to html2canvas options. For lazy-loaded images, wait for them to load using the Promise-based approach shown in the advanced example.

Problem: Cut-off Content

Cause: The HTML container is wider than the PDF page.

Solution: Set a fixed width on your print container that matches the PDF width minus margins. For A4 with 15mm margins: width: 180mm (210mm - 30mm).

Problem: Large File Size (10MB+)

Cause: High scale value or high image quality.

Solution: Reduce image.quality to 0.8 or lower. Consider usingscale: 1 for screen-resolution PDFs that don't need to be printed.

Problem: Fonts Look Wrong

Cause: Web fonts haven't finished loading.

Solution: Always await document.fonts.ready before generating the PDF.

8. Alternative Approaches

Native Print API

For the simplest use case, you can use the browser's native print dialog:

function printDocument() {
  window.print();
}

// User clicks print, then "Save as PDF" in the dialog

Pros: Zero dependencies, perfect CSS support. Cons: Requires user interaction with the print dialog.

jsPDF Direct (No HTML)

If you're generating simple documents programmatically (not from HTML), jsPDF alone can be more efficient:

import { jsPDF } from 'jspdf';

const doc = new jsPDF();
doc.setFontSize(22);
doc.text('Invoice #12345', 20, 20);
doc.setFontSize(12);
doc.text('Date: 2025-01-15', 20, 30);
doc.text('Total: $500.00', 20, 40);
doc.save('invoice.pdf');

This approach is faster and produces smaller files, but you lose the ability to use HTML/CSS for layout.

Server-Side When Needed

For automated batch processing or background jobs, server-side tools like Puppeteer are still the right choice. See our alternatives comparison for a detailed breakdown.

Conclusion

Client-side PDF generation with JavaScript has matured significantly. With libraries like html2pdf.js, you can build rich, privacy-focused document tools that work entirely in the browser. Remember these key points:

  • Always wait for fonts and images to load before generating
  • Use print-specific CSS to optimize output
  • Control page breaks explicitly for long documents
  • Balance quality vs. file size with the scale and quality options

Ready to try it without writing code? Use our free online converter to see how your HTML looks as a PDF immediately.

Related Articles