How to Convert HTML to PDF in JavaScript
Choose browser printing, html2pdf.js, Puppeteer, or pdf-lib based on your runtime and output needs. Get runnable examples and fixes for common PDF issues.
To convert HTML to PDF in JavaScript, first choose where the conversion should run. For a user saving the current page, use the browser print dialog with print CSS. For a browser-side download of an element, use html2pdf.js and accept its image-based output. For server-side generation in Node.js, use Puppeteer and page.pdf(). Use pdf-lib when you need to create or edit PDF objects directly, rather than render arbitrary HTML and CSS.
The examples below target these different jobs. That distinction matters: a browser download and a server-generated document have different runtime, layout, and reliability constraints.
1. Choose the right JavaScript approach
| Approach | Runs where | Use it for | Main tradeoff |
|---|---|---|---|
| Browser print | In the user’s browser | Letting a user print or save a page as PDF | The browser controls the dialog and final settings; design and test print styles. |
| html2pdf.js | Browser only | A quick client-side download of a page element | It renders content as an image, so PDF text is not selectable or searchable; large canvases can fail. |
Puppeteer page.pdf() |
Node.js controlling a browser | Automated or server-side PDF generation from a page | Print CSS media is used by default; manage browser lifecycle, assets, print layout, and deployment. |
| pdf-lib | Browser and supported JavaScript runtimes | Creating or editing pages, text, images, and forms in a PDF | It is not a browser layout engine for arbitrary HTML and CSS. |
For a downloadable invoice with selectable text, prefer a browser print flow or Puppeteer. For simple client-side capture where image-like output is acceptable, html2pdf.js may fit. For merging, filling, or drawing PDF content, consider pdf-lib. See the html2pdf.js project documentation, Puppeteer PDF API, Puppeteer PDF guide, and pdf-lib documentation.
2. Let the user print the page from a browser
Use this when the page is already rendered in the browser and the user should choose the destination and print settings. Call window.print() from a button, and use @media print rules to remove controls and arrange printable content.
<button type="button" id="save-pdf">Print or save as PDF</button>
<main class="report">
<h1>Quarterly report</h1>
<p>This content will be included in the printed document.</p>
</main>
<script>
document.getElementById('save-pdf').addEventListener('click', () => {
window.print();
});
</script>
<style>
@media print {
#save-pdf, nav, .screen-only { display: none !important; }
.report { max-width: none; margin: 0; }
h1 { break-after: avoid; }
.new-page { break-before: page; }
}
</style>
The browser’s print interface handles saving to PDF. Add and test print-specific styles for the browsers you support. Check page breaks, headers, navigation, background colors, and content that should not appear on paper.
3. Create a client-side PDF with html2pdf.js
html2pdf.js is a browser-side convenience pipeline that uses html2canvas and jsPDF. Install it in a frontend project:
npm install --save html2pdf.js
Then import it in browser code and pass the element to render:
import html2pdf from 'html2pdf.js';
const element = document.getElementById('element-to-print');
if (!element) {
throw new Error('Could not find #element-to-print');
}
html2pdf().from(element).save('report.pdf');
The project also documents the short form html2pdf(element). This package must run in a browser; it is not a Node.js HTML-to-PDF solution. Its documented rendering process places an image in the PDF, so the resulting text is not selectable or searchable. Content may be resized or reflowed to fit pages, and excessively large canvas dimensions can produce blank output. Test long pages, web fonts, images, page breaks, and complex CSS in the target browser. See the html2pdf.js README and known limitations.
4. Generate a PDF from HTML in Node.js with Puppeteer
Puppeteer launches a browser, navigates to a page, and writes the rendered page as a PDF. Install Puppeteer in your Node.js project:
npm install puppeteer
Save the following as generate-pdf.js and run it with node generate-pdf.js https://example.com. It validates the URL and output path, waits for navigation, writes the PDF, and closes the browser even when an operation fails.
const puppeteer = require('puppeteer');
async function main() {
const input = process.argv[2];
const output = process.argv[3] || 'output.pdf';
if (!input) throw new Error('Usage: node generate-pdf.js <url> [output.pdf]');
const url = new URL(input);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('URL must use http or https');
}
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url.href, { waitUntil: 'networkidle2' });
await page.pdf({ path: output, format: 'A4', printBackground: true });
console.log(`Wrote ${output}`);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
page.pdf() uses print CSS media by default and waits for fonts by default. If the page’s design depends on screen media, call await page.emulateMediaType('screen') before generating the PDF. Printing adjusts colors by default; for exact print colors, the Puppeteer API points to the CSS property -webkit-print-color-adjust. For example:
@media print {
.brand-color {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
Set printBackground: true when background graphics should be included. Define paper size and page breaks in CSS or through the PDF options. Consult the PDF API reference for available options and the PDF generation guide for the documented browser lifecycle.
Wait for content your page loads after navigation
networkidle2 is a navigation condition, not proof that every application-specific task has finished. If your page fetches data or reveals content after a client-side event, wait for a stable selector or an application-ready signal before calling page.pdf().
await page.goto(url.href, { waitUntil: 'networkidle2' });
await page.waitForSelector('[data-pdf-ready="true"]', { timeout: 15000 });
await page.pdf({ path: output, format: 'A4', printBackground: true });
Use a selector that only appears when the content is ready. Avoid an arbitrary long delay as the only readiness check: it wastes time on fast pages and may still be too short on slow ones.
5. Create or edit PDF documents with pdf-lib
Choose pdf-lib when the output is built from PDF primitives or when an existing PDF needs modification. It can create and modify PDF documents across JavaScript environments, but it does not render an arbitrary web page’s CSS layout.
npm install pdf-lib
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const fs = require('node:fs/promises');
async function main() {
const pdf = await PDFDocument.create();
const page = pdf.addPage([595.28, 841.89]); // A4 points
const font = await pdf.embedFont(StandardFonts.Helvetica);
page.drawText('Quarterly report', {
x: 48,
y: 780,
size: 20,
font,
color: rgb(0.12, 0.2, 0.35),
});
const bytes = await pdf.save();
await fs.writeFile('report.pdf', bytes);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The pdf-lib documentation covers creating and loading documents, drawing text and images, forms, and other PDF operations. Use a browser engine such as Puppeteer when the source of truth is an HTML/CSS layout.
6. Configure layout, assets, and page output
Use print CSS deliberately
For browser printing and Puppeteer’s default PDF mode, create a dedicated print layout. Hide screen-only controls, control page breaks, set margins, and make wide tables or code blocks fit the page. The browser print dialog may let a user change settings, so test the actual supported browser and user flow.
@page { size: A4; margin: 16mm; }
@media print {
.no-print { display: none !important; }
.keep-together { break-inside: avoid; }
.start-new-page { break-before: page; }
}
Account for fonts and images
Wait for the page’s content and assets to be ready before capture. Puppeteer’s PDF workflow waits for fonts by default, but application data, remote images, and lazy-loaded sections may need their own readiness check. If a font or image is missing, check its URL, network access, and whether it has finished loading before PDF generation.
Choose text fidelity or image-like output
Browser printing and Puppeteer use browser print rendering. html2pdf.js documents image-based pages, which are unsuitable when readers must search, copy, or select the PDF text. The choice should follow the document’s real use: visual snapshot or usable text document.
7. Troubleshoot common conversion problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank or incomplete PDF | The page is still loading data, images, or lazy content. | Wait for a page-specific ready selector; verify image and API requests complete before printing. |
| Colors or backgrounds are missing | Print color adjustment or background printing is disabled. | For Puppeteer, set printBackground: true; use -webkit-print-color-adjust: exact where exact colors matter. |
| Layout differs from the screen | PDF rendering uses print media by default, and print CSS can change layout. | Fix @media print styles. If screen media is required in Puppeteer, emulate screen before calling page.pdf(). |
| Text cannot be selected or searched | html2pdf.js produces image-based output. | Use browser printing or Puppeteer for browser-rendered text output. |
| html2pdf.js returns blank output on a long page | The HTML canvas may exceed browser size limits. | Reduce the captured content or split it into smaller sections; test representative page lengths in the target browser. |
| Fonts look wrong or fall back | The font did not load, is inaccessible, or conversion started too early. | Check font requests and wait for the page’s font and content readiness before generating. |
| Node reports Puppeteer launch failure | The browser could not start in the current runtime or deployment environment. | Check that the deployed environment supports the launched browser and review Puppeteer’s installation and deployment guidance for that environment. |
| The script hangs or uses resources after an error | The browser is not closed on every code path. | Put browser closure in a finally block, as in the example. |
| pdf-lib output does not match the web page | pdf-lib draws PDF objects; it does not lay out arbitrary HTML and CSS. | Use Puppeteer or browser printing for HTML layout; use pdf-lib for direct PDF editing and drawing. |
8. Performance, reliability, and cost considerations
- Browser print: No server-side browser process is needed for a user-initiated print flow. The user’s browser handles conversion and output settings.
- html2pdf.js: Conversion runs on the client and depends on browser canvas limits. Large pages and image-heavy content need particular care; split or simplify content if output fails.
- Puppeteer: A browser must launch and render the page for each generation workflow. Close it reliably, wait only for the content needed, and account for the browser runtime and deployment environment. The cited documentation does not establish hosting cost, throughput, or comparative performance figures.
- pdf-lib: Direct PDF construction avoids using it as an HTML rendering engine, but your code must define the document’s page content and layout.
- Cost: The approaches listed here are software libraries or browser features. Actual infrastructure cost for server-side browser generation depends on where and how you deploy; no provider pricing or benchmark is asserted here.
9. Or skip the browser setup
If your goal is a PDF screenshot of a web page, ScreenshotNeo can return a PDF from one GET request. It is a website screenshot API and MCP server from ScreenshotNeo; see the API documentation for request options.
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
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "format": "pdf"},
timeout=90,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
format: 'pdf',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned HTTP ${res.status}`);
await require('node:fs/promises').writeFile('page.pdf', Buffer.from(await res.arrayBuffer()));
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. PDF options and other capture settings are in the docs. Sign up for 1,000 free screenshots a month, with no card.
10. Frequently asked questions
Can JavaScript convert a local HTML string to PDF?
Yes. In a browser, render that HTML into a document or element before printing or using a browser-side capture library. In Node.js, load or set the content in a Puppeteer page and generate the PDF after the page is ready.
Which option creates searchable PDF text?
Use browser printing or Puppeteer’s browser PDF workflow when you need browser-rendered text. html2pdf.js documents image-based output.
Can I use pdf-lib to convert a web page?
Not as a drop-in HTML/CSS renderer. pdf-lib is for creating and modifying PDF structures; use a browser rendering workflow for web layouts.
Why does a Puppeteer PDF look different from the screen?
Puppeteer generates PDFs using print media by default, so print styles can change the page. Emulate screen media only when the screen design is the intended output, and check color handling.


