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

PDFKit does not convert arbitrary HTML and CSS into a browser-faithful PDF. In Node.js, it is an imperative drawing library: your code places text, images, links, vector graphics and SVG paths on a PDF document. The dependable way to use it with HTML is to define the subset your application supports, parse or template that content, and map each element to PDFKit calls. If you need modern CSS layout or client-side JavaScript to run as it would in a browser, use a browser-based renderer or an HTML-to-PDF service instead.

The choice is therefore straightforward: use PDFKit for controlled invoices, reports and forms whose layout you own; use a browser renderer when the input is an arbitrary website or depends on flexbox, grid, web fonts or JavaScript components.

What PDFKit actually renders

PDFKit creates a PDF through drawing operations. Its documented model exposes a readable PDFDocument stream, so you pipe that stream to a file or HTTP response, add content, and call doc.end() to finalize it.

Requirement PDFKit approach Better fit when this is required
Known headings, paragraphs and labels Map each node to font, fontSize and text. PDFKit
Images Resolve a local path, buffer or data URL, then call doc.image. PDFKit
Links Draw visible text and add a link rectangle after calculating its position. PDFKit
Lines, shapes and diagrams Use PDFKit vector methods or its path() API. PDFKit
Arbitrary modern CSS layout Implement layout, wrapping, margins and page breaks yourself. Browser-based renderer
Client-side JavaScript charts or components Not provided by PDFKit. Renderer with JavaScript support
Complete SVG fragments Use paths for simple data or svg-to-pdfkit for richer SVG. PDFKit plus the SVG adapter, or a browser

Do not confuse the Node package with a separate Ruby project also named PDFKit. The Ruby tool wraps wkhtmltopdf and accepts HTML, URLs or files; its PDFKit.new(...).to_pdf examples do not apply to the Node pdfkit package.

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

Install PDFKit and create a PDF

Install the Node package in your project:

npm install pdfkit

This minimal program creates an A4 PDF, writes it to output.pdf, and closes the stream correctly:

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();

The order matters. Create the destination, call doc.pipe(destination), add all content, then call doc.end(). Ending the document before all drawing calls can produce an incomplete file; omitting it leaves the output stream open.

Design a supported HTML subset

Start with the tags and attributes your documents actually need rather than promising full browser compatibility. A practical subset often includes headings, paragraphs, emphasis, unordered and ordered lists, images, anchors and explicit page-break markers.

Normalize the input before drawing

  1. Parse the HTML with an HTML parser or produce the same structure from a template.
  2. Walk the resulting tree and normalize nodes into a small internal model such as { type: 'heading', level: 1, text: '...' }.
  3. Resolve image sources to files, buffers or data URLs. Decide in advance whether remote URLs are allowed and how failures are reported.
  4. Carry style decisions as document rules—font, size, color, indentation and spacing—instead of trying to interpret every CSS declaration.
  5. Render each node while tracking the current cursor, available width and remaining page height.

Map common elements

HTML element Typical PDFKit mapping Implementation detail
h1–h3 font, fontSize, then text Apply a spacing rule before and after each heading.
p text with a constrained width Let PDFKit wrap lines, then add paragraph spacing.
strong, em Switch to a registered bold or italic font Restore the parent font after the child node.
ul, ol Draw a bullet or number, then indent the text Increase the left inset for wrapped lines.
img doc.image(source, options) Choose a maximum width and preserve the aspect ratio.
a Visible text plus doc.link Record the text position and dimensions before adding the link rectangle.
br A line break in the current text flow Do not treat it as a new page.
Page-break marker doc.addPage() Expose this as an explicit, supported feature.

Runnable template renderer in Node.js

The following example shows the mapping layer without pretending to parse arbitrary CSS. The blocks array is what your HTML parser or template adapter would produce. It supports headings, paragraphs, lists, images and links, and leaves page-break decisions visible in code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const fs = require('node:fs');
const PDFDocument = require('pdfkit');

const blocks = [
  { type: 'heading', level: 1, text: 'Quarterly report' },
  { type: 'paragraph', text: 'Revenue increased in the second quarter.' },
  { type: 'list', ordered: false, items: ['North America', 'Europe'] },
  { type: 'link', text: 'Open the source page', href: 'https://example.com' },
  { type: 'pageBreak' },
  { type: 'heading', level: 2, text: 'Appendix' }
];

const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('report.pdf'));

function renderBlock(block) {
  if (block.type === 'heading') {
    const sizes = { 1: 20, 2: 16, 3: 13 };
    doc.moveDown(0.5).fontSize(sizes[block.level] || 13).text(block.text);
    doc.moveDown(0.3);
    return;
  }

  if (block.type === 'paragraph') {
    doc.fontSize(11).text(block.text, { width: doc.page.width - 100 });
    doc.moveDown(0.5);
    return;
  }

  if (block.type === 'list') {
    block.items.forEach((item, index) => {
      const marker = block.ordered ? `${index + 1}.` : '•';
      doc.fontSize(11).text(`${marker} ${item}`, { indent: 14 });
    });
    doc.moveDown(0.5);
    return;
  }

  if (block.type === 'link') {
    const x = doc.x;
    const y = doc.y;
    doc.fontSize(11).fillColor('blue').text(block.text);
    doc.link(x, y, doc.widthOfString(block.text), doc.currentLineHeight(), block.href);
    doc.fillColor('black').moveDown(0.5);
    return;
  }

  if (block.type === 'image') {
    doc.image(block.source, { fit: [doc.page.width - 100, 300], align: 'center' });
    doc.moveDown(0.5);
    return;
  }

  if (block.type === 'pageBreak') {
    doc.addPage();
  }
}

blocks.forEach(renderBlock);
doc.end();

This is intentionally a renderer for a defined subset, not an HTML browser. A production adapter should also detect whether the next block fits, keep headings with at least one following line, carry list indentation across wrapped lines, and provide an error policy for missing images or unsupported tags.

Images, fonts, links and SVG

Images

Resolve every image before drawing. A local path, buffer or data URL can be passed to doc.image. Set a maximum width or height so a large source cannot push the cursor beyond the page. If an image cannot be decoded, fail the document with a useful source identifier or render an explicit placeholder; silently skipping it makes the PDF difficult to audit.

Fonts

When a particular typeface must survive on another machine, register and embed the font rather than relying on a viewer’s installed fonts. Keep the font choice in your renderer’s style rules so that switching from regular to bold or italic remains deterministic.

Links

PDFKit can draw the visible anchor text and add a link annotation. Save the text’s x and y before drawing, then calculate the rectangle from the rendered width and line height. For multi-line anchors, create one rectangle per line or constrain the link to a single line.

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

SVG

For simple SVG path data, PDFKit’s built-in path() method is enough. For complete fragments, the svg-to-pdfkit adapter accepts an SVG element or XML string and handles documented shapes, text and tspan, styling, colors, transforms and viewBox-related behavior.

npm install svg-to-pdfkit
const SVGtoPDF = require('svg-to-pdfkit');
const PDFDocument = require('pdfkit');
const fs = require('node:fs');

const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('diagram.pdf'));
const svgMarkup = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40"><rect width="100" height="40" fill="steelblue"/><text x="5" y="25" fill="white">Status</text></svg>';
SVGtoPDF(doc, svgMarkup, 50, 120, { width: 500 });
doc.end();

SVG that relies on browser-only CSS, external stylesheets or JavaScript still needs a browser renderer. Convert or inline those dependencies before passing the fragment to an SVG adapter.

Save to a file or stream over HTTP

Because a PDFDocument is a readable stream, the destination can be a file, an HTTP response or another writable stream. For an HTTP endpoint, set the PDF content type before piping:

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

http.createServer((req, res) => {
  if (req.url !== '/invoice.pdf') {
    res.writeHead(404).end('Not found');
    return;
  }
  res.writeHead(200, {
    'Content-Type': 'application/pdf',
    'Content-Disposition': 'inline; filename="invoice.pdf"'
  });
  const doc = new PDFDocument({ size: 'A4', margin: 50 });
  doc.pipe(res);
  doc.fontSize(18).text('Invoice');
  doc.fontSize(11).text('Generated as a stream.');
  doc.end();
}).listen(3000);

Do not call res.end() before PDFKit finishes; ending the PDF document closes the piped response.

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

When PDFKit is the wrong renderer

Move to a browser-based renderer when fidelity to an existing page matters more than direct drawing. That is the right choice for flexbox or grid layouts, responsive media queries, web-font loading, CSS print rules, client-rendered charts and pages whose content appears only after JavaScript executes.

A hosted service called pdfkitt documents a separate POST /v1/convert interface with exactly one html or url field, page-size and margin options, and an optional javascript flag for client-rendered pages. Its documented rendering cap is 30 seconds. Treat that service as a browser-style alternative, not as an API exposed by the Node PDFKit package.

Or skip the browser setup:

ScreenshotNeo is a website screenshot API and MCP server. Give it a URL with one GET request and it returns a clean PNG, JPEG, WebP or PDF. Before capture it accepts the cookie or consent banner like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and every response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers.

The basic request below follows the documented API shape. It saves the default WebP response; the same endpoint also supports PDF output. See the ScreenshotNeo documentation for the current output and option names.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

For AI-assisted workflows, ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info and capture_pdf tools that work with Claude, Cursor and other MCP clients. The service has 63 options, including full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper size, margins, landscape and page ranges, custom CSS and JavaScript, click-before-capture, selector hiding, waits for a selector, delay or network idle, request and resource blocking, custom headers, cookies, user agent and Authorization, timezone and geolocation, transparent backgrounds, image resizing, selectable cache TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

Plan Allowance Price
Free 1,000 shots per month Free, no card
Starter 3,000 shots $5
Growth 15,000 shots $15
Pro 60,000 shots $39
Scale 250,000 shots $99
Business 1,000,000 shots $249

Every feature is available on every plan, and yearly billing gives two months free. You can sign up for 1,000 free screenshots a month with no card and use ScreenshotNeo when reproducing a website in PDFKit would require browser layout work.

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

Troubleshooting PDFKit conversions

The output file is empty or unreadable

Check that the destination was piped before drawing and that doc.end() runs on every successful path. In an HTTP handler, set the content type before piping and do not terminate the response separately.

CSS changes have no effect

That is expected for declarations you have not implemented. Add an explicit style rule to your renderer, or switch to a browser renderer when the layout depends on CSS interactions that would be expensive to recreate.

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

Text overlaps or runs off the page

Use PDFKit’s width-constrained text, reserve space for margins, and measure the next block before drawing. Add a new page when the remaining height is insufficient. Keep long unbreakable strings, oversized images and nested lists in your test fixtures.

Images are missing

Verify that the resolved path or buffer exists in the process that generates the PDF and that the format is decodable. Log the source identifier and choose a deliberate failure policy instead of silently continuing.

SVG is incomplete

Replace browser-dependent SVG features with explicit paths or use svg-to-pdfkit for supported shapes, text, transforms and viewBox behavior. Inline external styles and assets before conversion.

The code throws an import or method error

Confirm that you installed the Node pdfkit package rather than the Ruby PDFKit toolchain. Also keep the import style consistent with the module format used by your project and the package version you installed.

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.

Performance, reliability and cost decisions

PDFKit keeps layout in your process, so performance depends on your document size, image decoding, font embedding and the amount of layout code you execute. For predictable workloads, reuse style rules, avoid unnecessarily large source images and stream directly to the final destination rather than buffering a second copy.

Reliability comes from making unsupported input visible: validate the allowed tags and attributes, put timeouts around any asset-fetching code you add, record which block failed, and test page breaks with the longest realistic content. PDFKit itself does not supply browser JavaScript execution, so adding more CSS declarations will not make a client-rendered page appear automatically.

Use a browser-style API when reproducing a live site is the primary job. ScreenshotNeo’s billing response distinguishes clean captures from bot checks, blank pages, timeouts, failed loads and cache hits, so those non-results are not billed. Its free allowance is 1,000 shots each month without a card, with paid plans starting at $5 for 3,000 shots.

Practical decision checklist

  • Choose PDFKit when you own the template and can describe the layout as drawing operations.
  • Define and document the HTML subset before accepting user content.
  • Normalize nodes, resolve assets, measure available space and make page breaks explicit.
  • Use built-in paths or svg-to-pdfkit for SVG that fits your supported feature set.
  • Stream with doc.pipe(...) and always finalize with doc.end().
  • Choose a browser renderer or ScreenshotNeo when CSS fidelity, JavaScript execution or live-site cleanup is essential.

Frequently Asked Questions

Can one PDF mix text, raster images and SVG?

Yes. Draw text and raster images with PDFKit, then add simple paths directly or pass supported SVG markup through svg-to-pdfkit. Keep each asset type within the feature set your renderer handles.

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

How should I handle an HTML tag my renderer does not support?

Reject it, transform it into a supported node, or emit a documented placeholder. Silent omission makes the resulting PDF hard to verify.

Is the Node PDFKit package the same as Ruby PDFKit?

No. Ruby PDFKit wraps wkhtmltopdf; Node pdfkit is an imperative PDF-generation library. Their APIs and rendering behavior are different.

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.