html-pdf, html2pdf.js, jspdf, and pdfmake are four distinct approaches to generating PDF documents within the JavaScript ecosystem. html-pdf acts as a wrapper around PhantomJS to render HTML/CSS server-side, though it is now deprecated. html2pdf.js combines jspdf and html2canvas to capture a visual snapshot of DOM elements in the browser. jspdf is a low-level library for programmatically drawing PDF content (text, lines, images) from scratch. pdfmake uses a custom declarative JSON structure to define document layout, handling pagination and styling internally without relying on HTML/CSS rendering.
Creating PDFs in JavaScript is a common requirement, but the ecosystem offers vastly different tools depending on whether you need to render HTML, draw shapes manually, or define structured documents. The packages html-pdf, html2pdf.js, jspdf, and pdfmake represent four distinct architectural patterns. Understanding their underlying engines is critical to avoiding performance pitfalls and maintenance nightmares.
html-pdf is the only package in this list that you should avoid entirely for new development. It relies on PhantomJS, a headless browser engine that was discontinued in 2018. The npm package is officially deprecated, and it fails to run on modern Node.js versions without significant hacking. It also carries unpatched security vulnerabilities.
If your legacy system uses html-pdf, you must plan a migration. The modern equivalent for server-side HTML rendering is using Puppeteer or Playwright to control a real Chrome/Chromium instance.
// ❌ DO NOT USE: html-pdf (Deprecated/Unmaintained)
const pdf = require('html-pdf');
pdf.create('<h1>Hello</h1>').toFile('out.pdf', (err, res) => {
// This will likely fail on Node 18+
});
// ✅ MODERN ALTERNATIVE: Puppeteer (Server-side HTML)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent('<h1>Hello</h1>');
await page.pdf({ path: 'out.pdf', format: 'A4' });
await browser.close();
})();
The remaining three active libraries split into two camps: those that convert HTML/DOM to images (html2pdf.js) and those that construct PDFs using native drawing commands or declarative structures (jspdf, pdfmake).
html2pdf.js: The DOM Screenshot Approachhtml2pdf.js is a convenience wrapper that chains html2canvas and jspdf. It takes a DOM element, renders it to an HTML5 Canvas (essentially taking a screenshot), and then places that image into a PDF.
// html2pdf.js: Captures the visual look of an element
const element = document.getElementById('invoice-preview');
html2pdf().from(element).save();
// Under the hood, this does:
// 1. html2canvas(element) -> <canvas>
// 2. jspdf.addImage(canvasData) -> PDF
jspdf: The Low-Level Canvas Approachjspdf does not understand HTML or CSS. It provides a coordinate-based API to draw lines, text, and images. You are responsible for calculating where every pixel goes. If you want a line, you define start/end coordinates. If you want text to wrap, you must calculate the width manually.
// jspdf: Manual coordinate drawing
import jsPDF from 'jspdf';
const doc = new jsPDF();
// You must specify exact X, Y coordinates
doc.text('Invoice #123', 10, 10);
doc.line(10, 15, 200, 15); // Draw a line manually
// Text wrapping requires manual calculation or plugins
doc.text('This is a long description that might wrap...', 10, 25, {
maxWidth: 180
});
doc.save('invoice.pdf');
pdfmake: The Declarative Document Approachpdfmake sits in the middle. It doesn't render HTML, but it doesn't require manual coordinates either. You define the document structure using a JSON-like definition (DD). It handles the heavy lifting of layout, table calculations, and pagination automatically.
// pdfmake: Declarative structure definition
import pdfMake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';
pdfMake.vfs = pdfFonts;
const docDefinition = {
content: [
{ text: 'Invoice #123', style: 'header' },
{
table: {
body: [
['Item', 'Price'],
['Widget', '$10.00'],
['Gadget', '$25.00']
]
}
}
],
styles: {
header: { fontSize: 18, bold: true }
}
};
pdfMake.createPdf(docDefinition).download('invoice.pdf');
One of the hardest problems in PDF generation is pagination. How does the library know when to start a new page?
jspdf offers no automatic pagination. If your text runs off the bottom of the page, it simply disappears or overlaps. You must write logic to measure text height and call doc.addPage() manually.
// jspdf: Manual pagination logic required
let y = 10;
const pageHeight = doc.internal.pageSize.height;
longText.split('\n').forEach(line => {
if (y > pageHeight - 20) {
doc.addPage();
y = 20;
}
doc.text(line, 10, y);
y += 10;
});
pdfmake handles pagination automatically. If a table row or paragraph doesn't fit on the current page, it moves the entire block to the next page. You can even keep table headers visible on every page.
// pdfmake: Automatic pagination with repeating headers
const docDefinition = {
content: [
{
table: {
headerRows: 1,
body: [
['Name', 'Age'], // This repeats on every new page
...generate100Rows() // If this overflows, it breaks pages cleanly
]
}
}
]
};
html2pdf.js relies on the browser's print media queries. You can use CSS @media print and page-break-inside: avoid to hint at where breaks should occur, but since it ultimately rasterizes the DOM, complex break logic can sometimes result in cut-off images or awkward spacing.
// html2pdf.js: CSS-based page break hints
const css = `
.row { page-break-inside: avoid; }
.new-page { page-break-before: always; }
`;
// Inject CSS into the DOM before capturing
Your choice often depends on where your styling logic lives.
If your team is expert in CSS, html2pdf.js (or a Puppeteer solution) feels natural. You style a div exactly as you want the PDF to look.
/* Used by html2pdf.js */
.invoice-box {
border: 1px solid #eee;
box-shadow: 0 0 10px rgba(0,0,0,0.15);
padding: 30px;
}
If you prefer JavaScript objects and want consistent rendering across server and client without a DOM, pdfmake is superior. You define styles once and apply them by name.
// Used by pdfmake
styles: {
title: { fontSize: 24, color: '#333', margin: [0, 0, 0, 20] },
subtotal: { bold: true, fillColor: '#eeeeee' }
}
If you need vector graphics, custom lines, or precise positioning that CSS cannot easily achieve (like drawing a diagonal watermark across the page), jspdf is the only choice.
// Used by jspdf
// Draw a diagonal watermark
doc.setLineWidth(0.5);
doc.setDrawColor(200);
doc.line(0, 0, 210, 297); // A4 dimensions in mm
| Feature | html2pdf.js | jspdf | pdfmake | html-pdf |
|---|---|---|---|---|
| Input Format | HTML DOM Element | JS API (Coordinates) | JSON Definition (DD) | HTML String |
| Rendering Engine | Canvas (Raster) | Vector Drawing | Vector Drawing | PhantomJS (Dead) |
| Text Selectable? | ❌ No (Image) | ✅ Yes | ✅ Yes | ✅ Yes |
| Pagination | CSS Hints | Manual Code | Automatic | Automatic (CSS) |
| Complex Layouts | Easy (via CSS) | Hard (Manual Math) | Medium (Table logic) | Easy (via CSS) |
| Status | Active | Active | Active | Deprecated |
Choosing the right tool comes down to your source data and output requirements:
html2pdf.js. It's the fastest path from DOM to PDF, accepting the trade-off of non-selectable text.pdfmake. Its declarative syntax and automatic pagination make it the most maintainable choice for structured documents.jspdf. It gives you total control, provided you are willing to manage the layout math yourself.html-pdf immediately. Switch to Puppeteer for server-side HTML rendering to ensure security and modern CSS support.Final Thought: There is no "best" library, only the best fit for your data structure. If your data is already in HTML, stick to HTML-based rendering (via Puppeteer or html2pdf). If your data is in a database, transforming it to pdfmake's JSON format often yields the highest quality, professional results.
Do NOT choose html-pdf for any new project. It relies on PhantomJS, which has been discontinued since 2018, and the package itself is deprecated. Using it introduces severe security vulnerabilities and compatibility issues with modern Node.js versions. You should immediately migrate existing implementations to puppeteer or playwright if you need server-side HTML rendering.
Choose html2pdf.js when you need a quick, client-side solution to export an existing HTML view (like a dashboard or invoice preview) exactly as it appears on screen. It is ideal for 'what you see is what you get' scenarios where precise pixel-perfect reproduction of complex CSS is required, and you can tolerate potential text blurriness or large file sizes due to canvas rasterization.
Choose jspdf when you need fine-grained, programmatic control over every element in the PDF, such as drawing custom shapes, manipulating raw coordinates, or adding interactive forms. It is best suited for generating simple reports, tickets, or certificates where you build the layout code-by-code rather than relying on HTML/CSS or a declarative schema.
Choose pdfmake when you need to generate structured, text-heavy documents (like invoices, reports, or books) with reliable pagination, tables, and headers/footers. Its declarative JSON approach separates content from layout logic, making it easier to maintain complex document structures than manual coordinate plotting, while avoiding the fragility of HTML-to-PDF conversion.

Example Business Card
-> and its Source file
Have a look at the releases page: https://github.com/marcbachmann/node-html-pdf/releases
Install the html-pdf utility via npm:
$ npm install -g html-pdf
$ html-pdf test/businesscard.html businesscard.pdf
var fs = require('fs');
var pdf = require('html-pdf');
var html = fs.readFileSync('./test/businesscard.html', 'utf8');
var options = { format: 'Letter' };
pdf.create(html, options).toFile('./businesscard.pdf', function(err, res) {
if (err) return console.log(err);
console.log(res); // { filename: '/app/businesscard.pdf' }
});
var pdf = require('html-pdf');
pdf.create(html).toFile([filepath, ]function(err, res){
console.log(res.filename);
});
pdf.create(html).toStream(function(err, stream){
stream.pipe(fs.createWriteStream('./foo.pdf'));
});
pdf.create(html).toBuffer(function(err, buffer){
console.log('This is a buffer:', Buffer.isBuffer(buffer));
});
// for backwards compatibility
// alias to pdf.create(html[, options]).toBuffer(callback)
pdf.create(html [, options], function(err, buffer){});
html-pdf can read the header or footer either out of the footer and header config object or out of the html source. You can either set a default header & footer or overwrite that by appending a page number (1 based index) to the id="pageHeader" attribute of a html tag.
You can use any combination of those tags. The library tries to find any element, that contains the pageHeader or pageFooter id prefix.
<div id="pageHeader">Default header</div>
<div id="pageHeader-first">Header on first page</div>
<div id="pageHeader-2">Header on second page</div>
<div id="pageHeader-3">Header on third page</div>
<div id="pageHeader-last">Header on last page</div>
...
<div id="pageFooter">Default footer</div>
<div id="pageFooter-first">Footer on first page</div>
<div id="pageFooter-2">Footer on second page</div>
<div id="pageFooter-last">Footer on last page</div>
config = {
// Export options
"directory": "/tmp", // The directory the file gets written into if not using .toFile(filename, callback). default: '/tmp'
// Papersize Options: http://phantomjs.org/api/webpage/property/paper-size.html
"height": "10.5in", // allowed units: mm, cm, in, px
"width": "8in", // allowed units: mm, cm, in, px
- or -
"format": "Letter", // allowed units: A3, A4, A5, Legal, Letter, Tabloid
"orientation": "portrait", // portrait or landscape
// Page options
"border": "0", // default is 0, units: mm, cm, in, px
- or -
"border": {
"top": "2in", // default is 0, units: mm, cm, in, px
"right": "1in",
"bottom": "2in",
"left": "1.5in"
},
paginationOffset: 1, // Override the initial pagination number
"header": {
"height": "45mm",
"contents": '<div style="text-align: center;">Author: Marc Bachmann</div>'
},
"footer": {
"height": "28mm",
"contents": {
first: 'Cover page',
2: 'Second page', // Any page number is working. 1-based index
default: '<span style="color: #444;">{{page}}</span>/<span>{{pages}}</span>', // fallback value
last: 'Last Page'
}
},
// Rendering options
"base": "file:///home/www/your-asset-path/", // Base path that's used to load files (images, css, js) when they aren't referenced using a host
// Zooming option, can be used to scale images if `options.type` is not pdf
"zoomFactor": "1", // default is 1
// File options
"type": "pdf", // allowed file types: png, jpeg, pdf
"quality": "75", // only used for types png & jpeg
// Script options
"phantomPath": "./node_modules/phantomjs/bin/phantomjs", // PhantomJS binary which should get downloaded automatically
"phantomArgs": [], // array of strings used as phantomjs args e.g. ["--ignore-ssl-errors=yes"]
"localUrlAccess": false, // Prevent local file:// access by passing '--local-url-access=false' to phantomjs
// For security reasons you should keep the default value if you render arbritary html/js.
"script": '/url', // Absolute path to a custom phantomjs script, use the file in lib/scripts as example
"timeout": 30000, // Timeout that will cancel phantomjs, in milliseconds
// Time we should wait after window load
// accepted values are 'manual', some delay in milliseconds or undefined to wait for a render event
"renderDelay": 1000,
// HTTP Headers that are used for requests
"httpHeaders": {
// e.g.
"Authorization": "Bearer ACEFAD8C-4B4D-4042-AB30-6C735F5BAC8B"
},
// To run Node application as Windows service
"childProcessOptions": {
"detached": true
}
// HTTP Cookies that are used for requests
"httpCookies": [
// e.g.
{
"name": "Valid-Cookie-Name", // required
"value": "Valid-Cookie-Value", // required
"domain": "localhost",
"path": "/foo", // required
"httponly": true,
"secure": false,
"expires": (new Date()).getTime() + (1000 * 60 * 60) // e.g. expires in 1 hour
}
]
}
The full options object gets converted to JSON and will get passed to the phantomjs script as third argument.
There are more options concerning the paperSize, header & footer options inside the phantomjs script.