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.jsOr 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 fileimage.quality: JPEG quality (0.1 to 1.0) - higher means better quality but larger fileshtml2canvas.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 elementmode: 'css': Respects CSSpage-break-*propertiesbefore: CSS selector for elements that should start on a new pageafter: CSS selector for elements that should be followed by a page breakavoid: 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
useRefinstead ofdocument.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
6. Print CSS Best Practices
While html2pdf.js captures the screen rendering, adding print-specific CSS improves the PDF output significantly. Here's a comprehensive print stylesheet:
/* Essential Print Styles */
@media print {
/* Hide UI elements that don't belong in PDFs */
nav, footer, .sidebar, .no-print, button {
display: none !important;
}
/* Reset colors for readability */
body {
background: white !important;
color: black !important;
font-size: 12pt;
line-height: 1.5;
}
/* Ensure background colors print */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Page break control */
h1, h2, h3 {
page-break-after: avoid;
}
table, figure, img {
page-break-inside: avoid;
}
.page-break {
page-break-before: always;
}
/* Ensure links are readable */
a {
text-decoration: underline;
color: #000;
}
/* Optionally show link URLs */
a[href^="http"]:after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #666;
}
}Key Concepts
- Hide non-essential elements: Navigation, buttons, and interactive elements don't belong in a PDF.
- Background colors: Browsers disable backgrounds by default to save ink. Use
print-color-adjust: exactto override this. - Page breaks: Use
page-break-inside: avoidon tables and images to prevent awkward splits. - Typography: Use points (pt) for font sizes in print. 12pt is standard for body text.
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 dialogPros: 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.