Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Node.js PDFKit does not render arbitrary HTML and CSS directly. It is an imperative PDF-generation library: you create a PDFDocument, draw text and graphics, place images, add links, and finish the stream. To convert HTML, parse or template a deliberately supported subset of elements, then map those nodes to PDFKit calls. If you need browser-level CSS layout or client-side JavaScript, use a browser renderer or an HTML-to-PDF service instead.

What PDFKit can—and cannot—convert

The Node package named pdfkit is designed for programmatic PDF creation, not as an HTML engine. Its documented primitives cover text, images, links, vector drawing and SVG paths. There is no official function that accepts an arbitrary HTML document and reproduces the browser’s layout.

That distinction determines your implementation:

  • Controlled templates: build a small HTML-like input format and map each supported node to PDFKit methods.
  • Modern CSS: use a browser-based renderer when flexbox, grid, complex pagination, web fonts or CSS counters must match a browser.
  • Client-side applications: choose a renderer that executes JavaScript when charts or components are created in the browser.

A separate Ruby project also called PDFKit wraps wkhtmltopdf and accepts HTML, URLs or files. Its PDFKit.new(...).to_pdf examples are not applicable to the Node pdfkit package.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Generate a PDF with PDFKit in Node.js

Install the package

npm install pdfkit

Create and save a document

const fs = require('node:fs');
const { PDFDocument } = require('pdfkit');

const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('output.pdf'));
doc.fontSize(18).text('Invoice');
doc.fontSize(11).text('Rendered from a supported HTML template.');
doc.end();

PDFDocument instances are readable Node streams. Piping the document to a file (or an HTTP response), adding content, and calling doc.end() finalizes the PDF. Omitting doc.end() leaves the output incomplete.

Stream to an HTTP response

app.get('/invoice.pdf', (req, res) => {
  res.setHeader('Content-Type', 'application/pdf');
  const doc = new PDFDocument({ size: 'A4', margin: 50 });
  doc.pipe(res);
  doc.fontSize(18).text('Invoice');
  doc.fontSize(11).text('Generated on demand.');
  doc.end();
});

The same pattern works in any Node HTTP framework: set the PDF content type, pipe to the response, write content, then end the document.

Map a supported HTML subset to PDFKit

A practical converter separates parsing from layout. Parse trusted HTML with the parser of your choice, walk the resulting tree, and implement only the tags and attributes your documents require. Keep unsupported CSS visible in your documentation rather than silently promising browser fidelity.

1. Define the supported elements

A common first version supports headings, paragraphs, unordered lists, images, anchors and explicit page breaks. Convert inline formatting to font changes only where you can restore the previous state reliably. Treat styles such as grid, flexbox, floats, sticky positioning, media queries and CSS animations as unsupported unless you implement them yourself.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Track layout state

Maintain the current cursor, left and right margins, line height, active font and page number. PDFKit wraps text when you provide a width, but your renderer still needs to decide when a block starts, how much vertical space it consumes and when to call doc.addPage(). For long paragraphs, use PDFKit’s measured height or let its text flow while reserving space for headers and footers.

3. Render text and headings

function renderHeading(doc, text, level) {
  const sizes = { 1: 22, 2: 16, 3: 13 };
  doc.moveDown(0.6);
  doc.fontSize(sizes[level] || 12).text(text, { width: 500 });
  doc.moveDown(0.25);
}

function renderParagraph(doc, text) {
  doc.fontSize(11).text(text, { width: 500, lineGap: 3 });
  doc.moveDown(0.4);
}

This example deliberately handles plain text. A production renderer should decode entities, normalize whitespace, enforce a maximum block width and sanitize any user-controlled content before parsing it.

4. Resolve images

Resolve an image source to a local filename, a buffer or a data URL, then call doc.image. Decide whether remote URLs are allowed; downloading arbitrary URLs introduces latency and server-side request risks. Supply width or height constraints so a large source image cannot exceed the page.

doc.image(imageBuffer, {
  fit: [500, 320],
  align: 'center',
  valign: 'center'
});

5. Turn links into clickable areas

Render the anchor’s visible text and calculate the rectangle occupied by that text before calling doc.link(x, y, width, height, href). Multi-line links require one rectangle per line or a more sophisticated text-layout pass. Do not expose untrusted schemes such as javascript:; allow only the URL schemes your application needs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

6. Fonts and page breaks

Register and embed fonts when the output must preserve a particular typeface or non-Latin characters. Add a page before a block that cannot fit, and provide an explicit page-break element in your input model for invoices, reports or chapters. Headers and footers should be rendered from a page event or a wrapper that knows the page dimensions.

Complete miniature HTML-to-PDF renderer

The following example accepts a small, predictable set of tags. It is intentionally not a general browser replacement; expand the switch only when you can test its pagination and security behavior.

const fs = require('node:fs');
const PDFDocument = require('pdfkit');

function textOf(node) {
  if (node.type === 'text') return node.data;
  return (node.children || []).map(textOf).join(' ');
}

function renderNode(doc, node) {
  if (node.type === 'text') {
    doc.fontSize(11).text(node.data, { width: 500, lineGap: 3 });
    return;
  }
  const children = node.children || [];
  switch (node.name) {
    case 'h1':
      doc.moveDown(0.6).fontSize(22).text(textOf(node), { width: 500 }).moveDown(0.3);
      break;
    case 'h2':
      doc.moveDown(0.5).fontSize(16).text(textOf(node), { width: 500 }).moveDown(0.2);
      break;
    case 'p':
      doc.fontSize(11).text(textOf(node), { width: 500, lineGap: 3 }).moveDown(0.4);
      break;
    case 'br':
      doc.moveDown(1);
      break;
    case 'ul':
      for (const li of children.filter(child => child.name === 'li')) {
        doc.fontSize(11).text('• ' + textOf(li), { width: 480, indent: 20 });
      }
      doc.moveDown(0.4);
      break;
    default:
      for (const child of children) renderNode(doc, child);
  }
}

const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('output.pdf'));
// Feed renderNode() nodes produced by your HTML parser here.
renderNode(doc, { name: 'h1', children: [{ type: 'text', data: 'Report' }] });
renderNode(doc, { name: 'p', children: [{ type: 'text', data: 'A supported paragraph.' }] });
doc.end();

In a real application, parse the HTML before calling renderNode, reject dangerous elements and attributes, and add tests for nested formatting, long words, images, links and page boundaries.

SVG from HTML

Simple paths

For basic SVG path data, PDFKit’s built-in path() API can draw the geometry directly. This is suitable when you control the paths and do not need the complete SVG document model.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Complete SVG fragments

For shapes, text and tspan, styling, colors, transforms and viewBox behavior, use the complementary svg-to-pdfkit package.

const SVGtoPDF = require('svg-to-pdfkit');

const svgMarkup = '<svg xmlns="http://www.w3.org/2000/svg" width="300" height="80">'
  + '<rect width="300" height="80" fill="#eee"/>'
  + '<text x="20" y="45">Hello SVG</text>'
  + '</svg>';

SVGtoPDF(doc, svgMarkup, 50, 120, { width: 500 });

Validate or sanitize SVG supplied by users. External references, embedded scripts and unexpectedly large dimensions should be rejected according to your threat model.

When PDFKit is the wrong renderer

Requirement Best fit Reason
Controlled templates and deterministic drawing PDFKit Direct text, image, vector and stream APIs
Arbitrary modern CSS layout Browser or API renderer Browser layout engines handle CSS more completely
Client-side JavaScript charts or components JavaScript-capable renderer PDFKit does not execute page JavaScript
Small server bundle and direct streaming PDFKit No browser process is required
Rich SVG diagrams svg-to-pdfkit or browser renderer Choose based on SVG complexity and fidelity needs

The hosted pdfkitt API is a separate service choice: its documented POST /v1/convert accepts exactly one html or url field, page-size and margin options, and an optional javascript flag. Its documented rendering cap is 30 seconds. Those options do not belong to the Node PDFKit library.

Or skip the browser setup

If your real goal is a clean screenshot or PDF of a live page rather than a hand-mapped PDFKit document, ScreenshotNeo provides a single-call API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and the response reports the page verdict and billing status in headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read the parameter reference in the ScreenshotNeo documentation. cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes full-page capture, element selectors, device presets, PDF page options, custom CSS and JavaScript, waits, request blocking, cookies and headers, geolocation, caching, signed links, asynchronous webhooks, bulk capture and a usage API on every plan. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting PDFKit conversions

The file is empty or corrupt

Confirm that the document is piped to a writable destination and that doc.end() is called exactly once after all content is added. Handle stream errors so a failed destination is not mistaken for a successful PDF.

Text overlaps or runs off the page

Give text a width, use measured heights for blocks, and track margins after every image or paragraph. Add page-break logic before content that cannot fit and test with long unbroken words.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Images do not appear

Check that the source resolves to a readable filename, buffer or data URL. Remote downloads must complete before doc.image runs; verify format support and constrain oversized images.

SVG styling is missing

Use path() only for simple paths. For complete fragments, pass valid SVG XML to svg-to-pdfkit and verify transforms, fonts and viewBox dimensions.

HTML looks different from the browser

That is expected when the input relies on unsupported CSS or JavaScript. Reduce the template to your documented subset, implement the missing layout deliberately, or switch to a browser-capable renderer.

The wrong PDFKit package was installed

Check whether your code is Node.js pdfkit or the Ruby wrapper around wkhtmltopdf. Their APIs and rendering models are unrelated.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Operational checklist

  • Pin and document the Node and package versions used by your build.
  • Sanitize HTML, URLs, images and SVG when any input is user-controlled.
  • Set download limits and timeouts for remote assets.
  • Test page breaks, embedded fonts, links, images, RTL or non-Latin text, and large documents.
  • Stream output where possible and monitor destination errors.
  • Choose PDFKit for controlled drawing; choose a browser renderer for browser fidelity.

Frequently Asked Questions

Does PDFKit accept an HTML string directly?

The Node PDFKit package does not provide an official arbitrary-HTML input method. Parse a supported subset and map it to PDFKit calls, or use a browser-based renderer.

Can PDFKit run the JavaScript in my web page?

No. Client-rendered charts and components require a renderer that executes page JavaScript.

How do I preserve a custom font?

Register and embed the font in PDFKit, then test the required characters and fallback behavior.

Is Ruby PDFKit the same as Node pdfkit?

No. Ruby PDFKit wraps wkhtmltopdf; Node pdfkit is an imperative PDF-generation library.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.