html-pdf, pdfkit, and puppeteer are three distinct approaches to generating PDF files within the JavaScript ecosystem. html-pdf is a legacy wrapper around PhantomJS that converts HTML strings to PDFs but is no longer maintained. pdfkit is a low-level PDF generation library that allows developers to draw vectors, text, and images directly using JavaScript commands without relying on a browser engine. puppeteer provides a high-level API to control headless Chrome or Chromium, enabling accurate HTML-to-PDF conversion by rendering actual web pages before saving them as documents.
Generating PDFs in Node.js is a common requirement for reports, invoices, and tickets. While html-pdf, pdfkit, and puppeteer all produce PDF files, they work in fundamentally different ways. Understanding their core mechanisms helps you avoid technical debt and performance issues.
html-pdf converts HTML strings into PDFs using PhantomJS.
// html-pdf: Legacy HTML rendering
const pdf = require('html-pdf');
const html = '<h1>Invoice</h1><p>Total: $100</p>';
pdf.create(html).toFile('./invoice.pdf', (err, res) => {
if (err) return console.error(err);
console.log('File saved');
});
pdfkit draws PDF elements using vector commands.
// pdfkit: Vector drawing
const PDFDocument = require('pdfkit');
const doc = new PDFDocument();
const fs = require('fs');
doc.pipe(fs.createWriteStream('./invoice.pdf'));
doc.fontSize(20).text('Invoice', 100, 100);
doc.fontSize(12).text('Total: $100', 100, 130);
doc.end();
puppeteer launches a headless Chrome browser to render content.
// puppeteer: Browser rendering
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent('<h1>Invoice</h1><p>Total: $100</p>');
await page.pdf({ path: './invoice.pdf', format: 'A4' });
await browser.close();
})();
The tools you need to install and run vary significantly between these packages.
html-pdf requires PhantomJS.
// html-pdf: Requires phantomjs binary
// Installation often fails on new OS versions
// npm install html-pdf
// Depends on phantomjs-prebuilt (deprecated)
pdfkit has no external binary dependencies.
// pdfkit: Pure JS
// npm install pdfkit
// No browser or binary needed
// Works in AWS Lambda, Vercel, etc.
puppeteer downloads a Chromium browser instance.
// puppeteer: Bundles Chromium
// npm install puppeteer
// Downloads Chrome binary automatically
// Requires system libraries (libgbm, etc.) on Linux
How you design the document changes based on the tool you pick.
html-pdf uses CSS for styling.
// html-pdf: CSS styling
const html = `
<style>.box { color: red; }</style>
<div class="box">Content</div>
`;
// Limited CSS support due to old WebKit engine
pdfkit uses coordinates and drawing commands.
// pdfkit: Coordinate styling
const y = 100;
doc.fillColor('red').text('Content', 50, y);
// Must manually calculate y + height for next element
puppeteer uses full modern CSS.
// puppeteer: Modern CSS
const html = `
<style>
.box { color: red; display: flex; }
@media print { .no-print { display: none; } }
</style>
<div class="box">Content</div>
`;
// Full CSS support via Chrome engine
Choosing a library involves considering how long it will remain viable.
html-pdf is deprecated.
// html-pdf: Deprecated
// Do not use for new projects
// Risk of security issues and build failures
pdfkit is actively maintained.
// pdfkit: Active
// Stable API
// Safe for production use
puppeteer is actively maintained by Google.
// puppeteer: Active
// Maintained by Chrome team
// Regular security updates
Despite their differences, these libraries share some common goals and patterns.
// All produce .pdf files
// html-pdf -> './file.pdf'
// pdfkit -> './file.pdf'
// puppeteer -> './file.pdf'
// pdfkit: Direct stream
const stream = fs.createWriteStream('file.pdf');
doc.pipe(stream);
// puppeteer: Buffer or path
const pdfBuffer = await page.pdf({ path: 'file.pdf' });
// html-pdf: Callback or stream
pdf.create(html).toStream((err, stream) => { /*...*/ });
// All require async handling
// html-pdf: Callback
// pdfkit: Stream events
// puppeteer: Async/Await
| Feature | html-pdf | pdfkit | puppeteer |
|---|---|---|---|
| Engine | PhantomJS (Deprecated) | Pure JS Vector | Headless Chrome |
| Input | HTML String | Drawing Commands | HTML + CSS |
| CSS Support | Limited (Old WebKit) | None | Full (Modern) |
| Dependencies | PhantomJS Binary | None | Chromium Binary |
| Status | β Deprecated | β Active | β Active |
html-pdf is a legacy tool β do not use it for new work. It relies on PhantomJS, which is dead technology. If you encounter it in an old codebase, plan to migrate away from it as soon as possible.
pdfkit is like a digital pen ποΈ β perfect for drawing structured documents from data. Use it for invoices, tickets, or certificates where you control every pixel and do not need HTML layouts.
puppeteer is like a robot browser π€ β ideal for converting web pages to PDFs. Use it when you have existing HTML templates or need complex CSS layouts that match your website exactly.
Final Thought: For most modern web applications, puppeteer offers the best balance of fidelity and maintenance. Choose pdfkit only if you need lightweight generation without browser overhead. Avoid html-pdf entirely to prevent future technical debt.
Avoid html-pdf for new projects because it depends on PhantomJS, which is deprecated and unmaintained. Only consider this if you are maintaining a legacy system that cannot be refactored immediately. For any modern development, choose a supported alternative like puppeteer to ensure security and compatibility.
Choose pdfkit when you need to generate structured documents like invoices, receipts, or reports from scratch without relying on HTML or CSS. It is ideal for scenarios where you need precise control over vector graphics, coordinates, and low-level PDF features without the overhead of a browser engine.
Choose puppeteer when you need to convert existing HTML pages or complex CSS layouts into PDFs with high fidelity. It is the best choice for rendering modern web designs, including flexbox, grid, and external stylesheets, because it uses a real Chrome instance to process the content.

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.