These five libraries represent the two main approaches to PDF generation in JavaScript: rendering HTML/CSS to PDF or drawing documents programmatically. html-pdf and html-pdf-node convert HTML strings to PDF using headless browsers (legacy). puppeteer controls a modern headless Chrome instance for high-fidelity HTML rendering. pdfkit and pdfmake generate PDFs from scratch using JavaScript commands or a specific document definition structure, offering precise control without HTML dependencies.
Generating PDFs in a JavaScript environment usually falls into two categories: rendering HTML/CSS or drawing elements programmatically. The five packages listed here cover both approaches, but they differ significantly in maintenance status, resource usage, and developer experience. Let's compare how they handle common engineering tasks.
html-pdf and html-pdf-node rely on PhantomJS, a headless browser that is no longer maintained. They take an HTML string and render it. This is easy for web developers but risky for long-term projects.
// html-pdf
const pdf = require('html-pdf');
pdf.create('<h1>Hello</h1>').toFile('out.pdf', (err, res) => {});
// html-pdf-node
const pdf = require('html-pdf-node');
pdf.create('<h1>Hello</h1>').toFile('out.pdf');
puppeteer controls a real, modern headless Chrome instance. It renders HTML/CSS exactly as a browser would, supporting modern standards like Flexbox and Grid.
// puppeteer
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' });
await browser.close();
})();
pdfkit and pdfmake do not use HTML. They draw directly to the PDF specification. pdfkit is low-level, while pdfmake uses a document definition object.
// pdfkit
const PDFDocument = require('pdfkit');
const doc = new PDFDocument();
doc.pipe(require('fs').createWriteStream('out.pdf'));
doc.text('Hello', 100, 100);
doc.end();
// pdfmake
const pdfmake = require('pdfmake/build/pdfmake');
const docDefinition = { content: ['Hello'] };
pdfmake.createPdf(docDefinition).download('out.pdf');
Dependency weight matters for serverless functions or containerized environments. Heavy browsers increase cold start times and image sizes.
html-pdf and html-pdf-node require PhantomJS binaries. This often leads to installation errors on modern Linux distributions or ARM architectures.
// html-pdf
// Requires phantomjs-prebuilt (deprecated)
npm install html-pdf
// html-pdf-node
// Similar binary dependencies
npm install html-pdf-node
puppeteer downloads a full Chromium browser by default. This is large (hundreds of MBs) but reliable. You can configure it to use a system browser to save space.
// puppeteer
// Downloads Chromium automatically
npm install puppeteer
// Or use slim version without browser
npm install puppeteer-core
pdfkit and pdfmake are pure JavaScript with no external browser binaries. They install quickly and run in any environment that supports Node.js.
// pdfkit
// Pure JS, no binaries
npm install pdfkit
// pdfmake
// Pure JS, no binaries
npm install pdfmake
How you style the output determines how easy it is for your team to maintain the code. Web developers prefer CSS; data-heavy apps often prefer JSON structures.
html-pdf and html-pdf-node use standard CSS. This is familiar but limited by PhantomJS's outdated CSS support (no Grid, limited Flexbox).
// html-pdf
const html = `<div style="display: flex;"><h1>Title</h1></div>`;
// Note: Flexbox support is poor in PhantomJS
// html-pdf-node
const html = `<div style="display: flex;"><h1>Title</h1></div>`;
// Same limitations as html-pdf
puppeteer supports full modern CSS. You can use external stylesheets, media queries for print, and complex layouts.
// puppeteer
await page.setContent(`
<style>@media print { .no-print { display: none; } }</style>
<div style="display: grid;"><h1>Title</h1></div>
`);
pdfkit requires manual positioning or using helper plugins for layout. You specify X and Y coordinates for text.
// pdfkit
doc.fontSize(25).text('Title', 100, 100);
doc.moveTo(100, 110).lineTo(500, 110).stroke();
pdfmake uses a content array with style objects. It handles line breaks and tables automatically without coordinates.
// pdfmake
const dd = {
content: [
{ text: 'Title', style: 'header' },
{ text: 'Body text', style: 'body' }
],
styles: { header: { fontSize: 25 } }
};
Choosing a library is a commitment to future maintenance. Deprecated tools introduce security risks and build failures.
html-pdf is officially deprecated. PhantomJS development stopped in 2018. Do not use this for new projects.
// html-pdf
// ⛔ DEPRECATED: Do not use in new projects
// Repository archived, no security updates
html-pdf-node is a community fork. It may work today but lacks the backing of a major organization. It is a stop-gap solution.
// html-pdf-node
// ⚠️ RISK: Community fork, long-term support uncertain
// Use only for legacy migration
puppeteer is maintained by the Chrome DevTools team. It updates regularly to match the latest Chrome features.
// puppeteer
// ✅ ACTIVE: Maintained by Google Chrome team
// Regular security and feature updates
pdfkit and pdfmake are stable, mature libraries. They change slowly because the PDF specification does not change often.
// pdfkit & pdfmake
// ✅ STABLE: Mature libraries with long-term support
// Ideal for stable document generation pipelines
Despite their differences, all these tools can generate valid PDF files for download or storage.
All packages can save directly to the disk or return a buffer for streaming.
// html-pdf
pdf.create(html).toFile('path/to/file.pdf');
// html-pdf-node
pdf.create(html).toFile('path/to/file.pdf');
// puppeteer
await page.pdf({ path: 'path/to/file.pdf' });
// pdfkit
doc.pipe(fs.createWriteStream('path/to/file.pdf'));
// pdfmake
pdfMake.createPdf(dd).download('file.pdf');
All five run in Node.js environments, making them suitable for backend APIs.
// All packages
// Express.js example pattern
app.get('/invoice', async (req, res) => {
// Generate PDF buffer
// res.send(buffer);
});
All tools run on the server, keeping template logic away from the client. However, browser-based tools (puppeteer, html-pdf) must sanitize HTML to prevent XSS if rendering user input.
// puppeteer (Sanitization required)
await page.setContent(sanitize(userInput));
// pdfkit (Safe by default)
doc.text(safeString, x, y);
| Feature | html-pdf | html-pdf-node | pdfkit | pdfmake | puppeteer |
|---|---|---|---|---|---|
| Engine | PhantomJS | PhantomJS | Pure JS | Pure JS | Chromium |
| Status | ⛔ Deprecated | ⚠️ Legacy Fork | ✅ Stable | ✅ Stable | ✅ Active |
| Input | HTML/CSS | HTML/CSS | Commands | JSON Def | HTML/CSS |
| CSS Support | Poor | Poor | N/A | N/A | Excellent |
| Size | Medium | Medium | Small | Small | Large |
html-pdf and html-pdf-node are legacy tools. They should only exist in your codebase during a migration away from them. Relying on PhantomJS in 2024 introduces unnecessary risk.
pdfkit is the choice for engineers who need to draw custom graphics or have precise control over every millimeter of the page. It has a steeper learning curve but offers maximum flexibility.
pdfmake strikes a balance. It is easier than pdfkit for standard documents but does not support HTML. It is perfect for internal reports where you control the data structure.
puppeteer is the modern standard for HTML-to-PDF. If your team knows CSS and HTML, this is the fastest path to high-quality output. The resource cost is higher, but the fidelity is unmatched.
Final Thought: For new projects, avoid the PhantomJS-based tools entirely. Choose puppeteer for web-like documents and pdfmake or pdfkit for data-driven reports.
Avoid this package for new projects as it is deprecated and relies on PhantomJS, which is no longer maintained. Only use it if you are maintaining a legacy system that cannot be refactored immediately. For any new work, migrate to puppeteer for HTML-based rendering.
Choose this only as a temporary bridge for legacy code that depends on the html-pdf API but needs to run on newer Node versions. It is not a long-term solution. Plan to refactor to puppeteer or a programmatic library like pdfkit for better stability.
Select pdfkit when you need low-level control over every element on the page, such as drawing custom vectors, lines, or positioning text at exact coordinates. It is ideal for generating invoices, tickets, or reports where layout precision matters more than HTML styling.
Pick pdfmake if you want to generate structured documents like reports or forms without writing HTML or managing low-level drawing commands. It uses a simple JSON-like definition to handle layout, making it easier to maintain than pdfkit for standard business documents.
Use puppeteer when you need pixel-perfect reproduction of existing HTML and CSS, such as converting web pages or complex email templates to PDF. It is the modern standard for HTML-to-PDF conversion, though it requires more resources due to the Chromium dependency.

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.