ScreenshotNeo

BlogHTML to image & PDF

How to Convert HTML to PDF With JavaScript Libraries

Choose Puppeteer for server-side webpage PDFs, html2pdf.js for browser exports, or PDFKit for documents built from structured data.

By the ScreenshotNeo team30 September 202610 min read

How to Convert HTML to PDF With JavaScript Libraries

For a real webpage with JavaScript and CSS, use Puppeteer with Chromium and page.pdf(). For an export button that runs in a visitor’s browser, use html2pdf.js. For a document whose layout you control in code, use PDFKit. If you want Chromium rendering without operating a browser yourself, use a hosted HTML-to-PDF API.

The right choice depends on where the code runs and whether you need to reproduce existing HTML or compose a new document. This guide covers the runnable paths, print styling, page breaks, fonts, resource loading, deployment, and common failures.

1. Choose the right JavaScript approach

Approach Best for Runs where Renders existing HTML? Main trade-off
Puppeteer Webpage or app output where browser rendering matters Node.js with Chromium Yes Browser deployment weight and operations
html2pdf.js A user-triggered export of a page element Browser only Yes, through a canvas-based pipeline Complex layouts, text selection, and memory need validation
PDFKit Invoices, reports, labels, and other controlled layouts Node.js or browser build No You draw and place content rather than print arbitrary HTML
Hosted Chromium API HTML or a URL rendered without managing Chromium locally Your app calls an external service Yes Network latency, credentials, vendor dependency, and data handling

For a general server-side conversion from a URL or HTML page, start with Puppeteer. It executes JavaScript in Chromium and prints the rendered page. Puppeteer’s guide specifically points to Page.pdf() for printing PDFs. [Puppeteer PDF generation guide]

2. Generate a PDF with Puppeteer in Node.js

Install Puppeteer in a Node project. The package’s browser installation and runtime requirements depend on the environment; use its current installation guidance when deploying to a container or server. [Puppeteer installation guide]

A browser-based renderer prints the page’s computed layout into PDF pages.
A browser-based renderer prints the page’s computed layout into PDF pages.
npm install puppeteer

Create convert.mjs:

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });

  await page.pdf({
    path: 'page.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true,
    margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' }
  });
} finally {
  await browser.close();
}

Run it with node convert.mjs https://example.com. The try/finally ensures Chromium is closed even when navigation or printing fails. For a service handling many requests, reuse a browser process where appropriate and create an isolated page per job; also enforce timeouts and concurrency limits.

Convert HTML content instead of a URL

Use page.setContent() when your application has already produced the HTML string. Include a base URL for relative assets, or use absolute URLs for stylesheets, images, and fonts.

import puppeteer from 'puppeteer';

const html = `
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { font: 16px Arial, sans-serif; margin: 24px; }
    h1 { color: #16324f; }
  </style>
</head>
<body><h1>Quarterly report</h1><p>Generated from HTML.</p></body>
</html>`;

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'networkidle0' });
  await page.pdf({ path: 'report.pdf', format: 'Letter', printBackground: true });
} finally {
  await browser.close();
}

For untrusted HTML, treat rendering as processing untrusted input: isolate the browser process, restrict its network access where possible, and avoid exposing secrets or privileged local files to page content.

Wait for the page to be ready

networkidle2 is a useful starting point, not a guarantee that an application has finished rendering. Analytics, streaming connections, or polling can keep network activity alive; an app may also render important content after the network becomes quiet. Prefer an application-specific readiness condition when available:

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForSelector('[data-pdf-ready="true"]', { timeout: 20000 });
await page.evaluate(() => document.fonts.ready);
await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });

If the site has no readiness marker, wait for a known selector or a short, bounded delay after navigation. Do not wait indefinitely for every network request: it can hang on pages with long-lived connections. Puppeteer waits for fonts by default during PDF generation, but explicitly checking document.fonts.ready can help diagnose timing-sensitive pages. [Puppeteer PDF generation guide]

3. Control print CSS, page size, and breaks

page.pdf() uses the CSS print media type. That means print-specific styles can alter visibility, layout, and colors. To capture the screen stylesheet instead, call await page.emulateMediaType('screen') before generating the PDF. [Puppeteer PDF API]

@media print {
  nav, .cookie-banner, .no-print { display: none !important; }
  body { color: #111; background: #fff; }
  h1, h2 { break-after: avoid; }
  .new-page { break-before: page; }
  table, figure { break-inside: avoid; }
}

@page {
  size: A4;
  margin: 16mm 14mm;
}

html {
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

Choose either CSS @page sizing or PDF options deliberately. preferCSSPageSize: true tells Puppeteer to prefer the page size specified by CSS. Without it, options such as format, width, and height define the PDF paper dimensions. Puppeteer modifies print colors by default; -webkit-print-color-adjust: exact requests exact colors where supported. [Puppeteer PDF options]

  • Paper: use format such as A4 or Letter, or set width and height.
  • Margins: use the PDF margin option or CSS @page margins and check their interaction.
  • Backgrounds: set printBackground: true to include CSS backgrounds.
  • Landscape: set landscape: true for a wider page.
  • Headers and footers: Puppeteer supports header and footer templates; reserve enough margin so they do not overlap document content.
  • Page breaks: use modern break properties such as break-before, break-after, and break-inside; test long tables and elements that cannot fit on one page.

Print output is not always identical to a browser screenshot. Inspect fonts, image loading, background colors, overflow, and break behavior in the same Chromium environment used for production.

4. Add a browser-side export with html2pdf.js

html2pdf.js is suitable when the person viewing a page should click “Export” and download a PDF without sending the content to your server. It uses html2canvas and jsPDF and runs in the browser; it does not run in Node.js. [html2pdf.js project documentation]

<button id="export">Download invoice</button>
<section id="invoice">
  <h1>Invoice</h1>
  <p>Invoice content goes here.</p>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
  document.querySelector('#export').addEventListener('click', async () => {
    const element = document.querySelector('#invoice');
    await html2pdf().set({
      margin: 0.4,
      filename: 'invoice.pdf',
      pagebreak: { mode: ['css', 'legacy'] },
      jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
    }).from(element).save();
  });
</script>

Pin and serve a reviewed library version according to your project’s dependency policy. Before adopting this path for important documents, inspect the downloaded PDF for selectable text, correct page breaks, cross-origin images, and acceptable memory use. Canvas-based rendering can rasterize content and behave differently from Chromium’s print pipeline, especially with large tables or long pages.

5. Build a PDF with PDFKit

PDFKit is a better fit when the application owns the document structure and can place text, images, and shapes directly. It is not an automatic HTML/CSS renderer: reproducing a complex webpage means rebuilding its layout using the PDFKit API. The project documents Node streams and a browser build, along with font and image support. [PDFKit getting started] [PDFKit project site]

npm install pdfkit
import PDFDocument from 'pdfkit';
import { createWriteStream } from 'node:fs';

const doc = new PDFDocument({ size: 'A4', margin: 48 });
doc.pipe(createWriteStream('report.pdf'));
doc.fontSize(20).text('Quarterly report');
doc.moveDown();
doc.fontSize(11).text('Revenue increased in the reporting period.');
doc.end();

For an HTTP endpoint, pipe the PDF document to the response and call doc.end() after writing all content. Handle stream errors and do not send a text or JSON response after PDF bytes have started. PDFKit’s stream model works well when the content can be generated incrementally.

6. Call an HTML-to-PDF API with cURL, Python, or Node.js

A hosted Chromium API accepts a URL or HTML and returns PDF bytes, so your application does not need to package Chromium. The documented html2pdf.app API uses an authenticated POST; check its current API documentation for the exact request fields, authentication format, limits, and error schema before using it. [html2pdf.app API documentation]

For any such service, verify the HTTP status and stream or save the binary response. Do not decode PDF bytes as UTF-8 text. Use bounded timeouts, protect credentials in server-side configuration, and review whether the HTML or URL contains data that may be sent to a third party.

7. Screenshot a webpage as a PDF

If your requirement is a visual record of a webpage rather than a paginated document with selectable text, a screenshot API can return a PDF capture. ScreenshotNeo offers PDF output and browser rendering through its API. The call below targets a webpage; consult the ScreenshotNeo API documentation for the available options and response behavior.

Document printing and screenshot-to-PDF capture solve different output needs.
Document printing and screenshot-to-PDF capture solve different output needs.
curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf

Keep in mind the distinction: printing HTML to PDF is usually for a multi-page document, while a screenshot PDF records rendered page appearance. Pick based on whether you need document pagination and text behavior or a visual capture.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One request captures a URL as a PDF, PNG, JPEG, or WebP. It removes cookie and consent banners, newsletter popups, and chat widgets before the capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers.

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

There is also an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free and make your first capture.

9. Troubleshooting HTML-to-PDF conversion

Symptom Likely cause What to try
PDF is blank or content is missing Navigation completed before client-side rendering, or the wrong element/state was used Wait for a specific ready selector or application signal; inspect the page before printing.
Fonts differ or fall back Font files failed, loaded late, or were inaccessible in the deployment environment Check font URLs and network responses; wait for document.fonts.ready.
Background colors disappear Print backgrounds are disabled or print CSS removes them Set printBackground: true and review @media print styles.
Wrong paper size or unexpected scaling PDF options and CSS @page rules conflict Set one intended size and use preferCSSPageSize deliberately.
Rows or headings split badly Content exceeds page height or break rules are missing Add print-specific break rules, reduce margins or spacing, and test long content.
Navigation times out Long-lived requests, slow resources, or an overly strict wait condition Use a less restrictive lifecycle event, then wait for a specific readiness signal with a separate timeout.
Chromium will not launch in production Browser executable or required runtime libraries are unavailable, or deployment constraints differ Follow Puppeteer’s installation guidance for the target image, verify executable access, and consider hosted rendering if operating Chromium is impractical.
html2pdf output is blurry or huge Canvas scale, page length, and rasterization affect resolution and memory Reduce the captured area, tune scale carefully, and compare against a browser print approach for long documents.
PDF response is corrupt Binary bytes were decoded as text, truncated, or mixed with an error response Check status and content type, preserve bytes, and avoid writing diagnostic output into the PDF stream.

10. Performance, reliability, and cost

With Puppeteer, Chromium startup and page rendering are significant parts of a conversion job. Reusing a browser can reduce repeated startup work, but isolate pages and cap concurrent jobs so a burst of large documents does not exhaust memory. Close pages after each job, set navigation and selector timeouts, and record failures by stage: launch, navigation, readiness, and PDF writing.

HTML-to-PDF work is sensitive to remote dependencies. Fonts, images, stylesheets, and scripts add network requests and can fail independently. For repeatable output, serve stable assets, wait for required resources, and use a deployment environment that matches production. A hosted API removes local browser operations but adds service latency, credentials, external availability, and data-processing considerations. PDFKit avoids browser rendering when the layout is programmatic, but engineering effort moves into layout and pagination code.

Cost includes more than a package license or API call: account for compute time, browser memory, storage and transfer of PDFs, operational maintenance, and the effort to maintain accurate page breaks. Benchmark with representative documents in your own environment; the source documentation does not establish a universal runtime or cost figure.

11. A practical decision checklist

  1. Does the source already exist as a rendered webpage? Choose Puppeteer or a hosted Chromium API.
  2. Must conversion happen on the visitor’s device? Try html2pdf.js and validate output quality on target browsers.
  3. Is the document assembled from application data with a controlled layout? Use PDFKit.
  4. Are exact print styles, text flow, or page breaks important? Use Chromium printing and own the print CSS.
  5. Are browser operations the main deployment obstacle? Compare the cost and data implications of a hosted renderer.
  6. Is the requested artifact a visual webpage record? Consider a screenshot-to-PDF endpoint and distinguish it from a print-layout PDF.

12. FAQ

Can JavaScript convert a local HTML file to PDF?

Yes. A Node.js Puppeteer script can navigate to a local file URL or load HTML with page.setContent(). Ensure relative assets resolve from an accessible base path.

Can Puppeteer generate a PDF in the browser?

Puppeteer controls Chromium through Node.js; it is not the usual choice for code running in a website visitor’s browser. For a client-side export button, use a browser library such as html2pdf.js.

Which option preserves selectable text?

Chromium’s print-to-PDF path and PDFKit produce document text, while html2pdf.js uses a canvas pipeline that can rasterize content. Inspect your actual output when text selection or accessibility matters.

Should I use a PDF library or a screenshot API?

Use a PDF library when you need document pagination and print layout. Use a screenshot API when the goal is to capture a webpage’s visual state as an image or PDF.