Convert HTML to PDF in JavaScript with html2pdf.js
Table of contents
Nutrient DWS handles rendering, fonts, and CSS for you — start with 50 free credits.
html2pdf.js converts a rendered HTML element into a PDF in the browser. Select the element, pass options to set(), and call from(element).save(). It uses html2canvas to capture the content and jsPDF to create the PDF, so the output’s text is an image rather than selectable text.
Use html2pdf.js(opens in a new tab) for downloadable receipts, invoices, and short reports generated from content already displayed on a webpage. You don’t need a conversion server, but you do need to wait for the content, fonts, and images to finish loading.
This tutorial includes a complete browser example, page-break options, and a React component. It also explains when a server-side renderer or Nutrient’s HTML-to-PDF API better fits the output you need.
Create a working html2pdf.js example
The example uses html2pdf.js 0.14.0. Create two files in an empty directory: index.html and index.js. Serve the directory over HTTP using your editor’s local web server, rather than opening it as a file:// URL.
Add the HTML content and download button
Save the following as index.html. The download button sits outside #content, so it won’t appear in the exported PDF:
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>HTML-to-PDF invoice</title> <style> body { margin: 24px; font-family: Arial, sans-serif; color: #222; } #content { max-width: 680px; background: white; padding: 20px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #999; padding: 10px; text-align: left; } </style> <script defer src="https://cdn.jsdelivr.net/npm/html2pdf.js@0.14.0/dist/html2pdf.bundle.min.js" ></script> <script defer src="index.js"></script> </head> <body> <div id="content"> <h1>Invoice #1234</h1> <p>Customer: Acme Company</p> <table> <thead> <tr><th>Item</th><th>Amount</th></tr> </thead> <tbody> <tr><td>Document review</td><td>$150.00</td></tr> </tbody> </table> <p>Total due: $150.00</p> </div> <button id="download" type="button">Download PDF</button> <p id="status" role="status"></p> </body></html>The bundled script includes its rendering dependencies. You can download that versioned bundle and host it yourself instead of using the content delivery network (CDN).
Generate the PDF on demand
Save this code as index.js. It checks that the target exists, waits for fonts and images, and reports conversion failures:
const button = document.getElementById('download');const status = document.getElementById('status');
button.addEventListener('click', async () => { button.disabled = true; status.textContent = 'Preparing PDF…';
try { const element = document.getElementById('content'); if (!element) throw new Error('The export element was not found.'); if (typeof html2pdf !== 'function') { throw new Error('The html2pdf.js script did not load.'); }
await document.fonts.ready; await Promise.all( Array.from(element.querySelectorAll('img'), (image) => image.decode()), );
await html2pdf() .set({ filename: 'invoice.pdf', margin: 10, image: { type: 'jpeg', quality: 0.95 }, html2canvas: { scale: 2, useCORS: true }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['css', 'legacy'], avoid: 'tr' }, }) .from(element) .save();
status.textContent = 'PDF generated.'; } catch (error) { status.textContent = 'Export failed: ' + error.message; } finally { button.disabled = false; }});Open the page through your local server and click Download PDF. Check the resulting invoice, including the table and margins. Selecting the text in the PDF won’t work because the content was captured as an image.

The screenshot shows output from the earlier example; the current code exports the invoice above.
Configure page size, margins, and quality
Pass conversion settings through set(). The options reference(opens in a new tab) documents the complete configuration.
| Option | Effect |
|---|---|
filename | Sets the downloaded file name. |
margin: 10 | Adds a margin of 10 in the units selected by jsPDF.unit — millimeters in this example. |
jsPDF.format and orientation | Select the page size and portrait or landscape layout. |
image.type and quality | Control the image embedded in the PDF. JPEG quality accepts values from zero to one. |
html2canvas.scale | Controls capture resolution. Higher values use more memory and can increase file size. |
html2canvas.useCORS | Attempts cross-origin image loading when the image server permits it. |
pagebreak | Controls where elements can begin or end pages. |
To export in landscape, change orientation to 'landscape' in the complete example. For US letter paper, set format to 'letter'.
The underlying html2canvas default scale(opens in a new tab) is the browser’s device pixel ratio, not always one. The example sets it explicitly to make the capture resolution predictable. Higher resolution improves raster detail but doesn’t create selectable text.
Control page breaks in multipage PDFs
For a forced break, add a class to the section that should begin a new page. For example, place this section inside #content after the invoice:
<section class="new-page"> <h2>Payment details</h2> <p>Please include invoice number 1234 with your payment.</p></section>Replace the pagebreak setting in the complete JavaScript example with:
pagebreak: { mode: ['css', 'legacy'], before: '.new-page', avoid: 'tr',},This is a property inside the existing options object, rather than a separate script. The selector forces a break before the payment section.
The css mode reads supported CSS break rules, while legacy handles the html2pdf__page-break class. Use avoid for elements you want to keep together. An element taller than a page still needs to split or be redesigned.
Avoid applying avoid-all as a universal fix. Test actual tables and long paragraphs, because keeping every element together can leave unwanted whitespace.
Use html2pdf.js with React or Next.js
Install the package in your React project:
npm install --save-exact html2pdf.js@0.14.0The component below uses a ref for the export element. It imports html2pdf.js inside the click handler so browser-only code isn’t evaluated during Next.js server rendering:
'use client';
import { useRef, useState } from 'react';
export default function Invoice() { const contentRef = useRef(null); const exportingRef = useRef(false); const [busy, setBusy] = useState(false); const [message, setMessage] = useState('');
async function downloadPDF() { if (exportingRef.current) return; exportingRef.current = true; setBusy(true); setMessage('');
try { const element = contentRef.current; if (!element) throw new Error('The invoice is not ready.');
const html2pdf = (await import('html2pdf.js')).default; await document.fonts.ready; await Promise.all( Array.from(element.querySelectorAll('img'), (image) => image.decode()), );
await html2pdf() .set({ filename: 'invoice.pdf', margin: 10, html2canvas: { scale: 2, useCORS: true }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, }) .from(element) .save();
setMessage('PDF generated.'); } catch (error) { setMessage('Export failed: ' + error.message); } finally { exportingRef.current = false; setBusy(false); } }
return ( <section> <div ref={contentRef} style={{ background: 'white', color: '#222', padding: 20 }}> <h1>Invoice #1234</h1> <p>Amount due: $150.00</p> </div> <button type="button" disabled={busy} onClick={downloadPDF}> {busy ? 'Exporting…' : 'Download PDF'} </button> <p role="status">{message}</p> </section> );}In a Next.js App Router project, save it as a component and render it from your page, or use it directly as src/app/page.js. In a browser-only React app, the 'use client' directive isn’t needed.
Troubleshoot blank PDFs and missing images
Check the source element and its assets before changing quality settings.
| Problem | What to check |
|---|---|
| Blank PDF | Confirm the selected element exists, is visible, and has nonzero dimensions. Wait for rendering and assets rather than adding an arbitrary timeout. |
| Missing images | Serve images from the same origin or configure cross-origin resource sharing (CORS) on their server. useCORS: true doesn’t bypass server restrictions. |
| Unsupported colors or missing CSS effects | Compare your styles against the html2canvas feature list(opens in a new tab). Simplify unsupported styles or use a browser print engine. |
| Blurry text | Increase scale after checking memory and file-size costs. Text remains rasterized. |
| Content split unexpectedly | Add targeted before or avoid selectors, reduce oversized elements, and inspect the page layout. |
| Export fails for a long document | Browser canvas dimensions and memory are limited. Split the content or use a server-side renderer. |
self is not defined in Next.js | Import html2pdf.js in a browser event handler or another browser-only boundary. |
| Form values appear as a picture | This export captures appearance; it doesn’t create interactive PDF form fields. |
There’s no universal safe page-count limit. Document dimensions, images, capture scale, and the browser all affect memory use. See the html2canvas FAQ(opens in a new tab) for canvas limits and cross-origin behavior.
When to use html2pdf.js
Choose html2pdf.js when the source is already on the page, a browser download is convenient, and image-based output is acceptable.
Consider another approach when readers need searchable text, accessible document structure, interactive form fields, or consistent pagination for long reports. Test the output against those requirements as well as checking its appearance.
For a broader comparison, see the following blog posts:
Convert HTML to PDF with Nutrient
Nutrient’s HTML-to-PDF API processes HTML on a server. This keeps the conversion workload off the user’s device and produces PDF text from rendered text rather than a screenshot of the entire page.
For styled templates, package the required fonts, CSS, and images according to the PDF generation guides. Check your actual output for layout and font requirements; browser-based rendering isn’t a guarantee that every CSS feature or page layout will behave identically.
Node.js example
This example uses Node.js 24’s built-in fetch, FormData, and Blob APIs. Create a self-contained index.html containing the document you want to convert. Keep the API key in your server environment as NUTRIENT_API_KEY; don’t put it in browser JavaScript.
Save the following as convert.mjs:
import { readFile, writeFile } from 'node:fs/promises';
async function convertHTML() { const apiKey = process.env.NUTRIENT_API_KEY; if (!apiKey) throw new Error('Set NUTRIENT_API_KEY before running this script.');
const form = new FormData(); form.append( 'html', new Blob([await readFile('index.html')], { type: 'text/html' }), 'index.html', );
const response = await fetch( 'https://api.nutrient.io/processor/generate_pdf', { method: 'POST', headers: { Authorization: 'Bearer ' + apiKey }, body: form, }, ); if (!response.ok) { throw new Error('Conversion failed (' + response.status + '): ' + await response.text()); }
await writeFile('result.pdf', Buffer.from(await response.arrayBuffer())); console.log('Saved result.pdf');}
convertHTML().catch((error) => { console.error(error.message); process.exitCode = 1;});After configuring the environment variable, run:
node convert.mjsThis request uses the html multipart field documented in the DWS Processor API getting started guide. This example buffers the response in memory; use a streaming implementation if your application needs to handle larger outputs.
Self-hosted conversion with Document Engine
For conversion on your own infrastructure, use Document Engine. With a running, licensed installation and an API token, the build endpoint can return a PDF:
curl --fail-with-body --request POST http://localhost:5000/api/build \ --header "Authorization: Token token=<API token>" \ --form 'page.html=@index.html' \ --form 'instructions={"parts":[{"html":"page.html"}]}' \ --output result.pdfThe multipart field name must match the html value in the instructions. For interactive fields, follow the separate fillable form generation guide and its supported HTML elements.
Compare the conversion approaches
| Requirement | html2pdf.js | Nutrient server conversion |
|---|---|---|
| Execution | Runs in the user’s browser | Runs through a cloud API or a self-hosted Document Engine |
| Text | Captured as an image | Rendered text can remain selectable |
| Styling | Uses html2canvas’s supported CSS subset | Uses server-side HTML rendering; verify templates and assets |
| Form fields | Captures visual appearance | Supports documented form-generation workflows |
| Setup | Browser script or npm dependency | Cloud API credentials or a licensed server installation |
| Licensing | MIT-licensed library | Commercial service or server license |
Create a DWS account(opens in a new tab) to evaluate the cloud API, or consult Processor API pricing for current credits and plan details.
Other HTML-to-PDF options
Puppeteer(opens in a new tab) and Playwright(opens in a new tab) can print webpages to PDF on a server. In Playwright, PDF generation uses Chromium. Test print styles, fonts, and page breaks with the browser version you deploy.
jsPDF(opens in a new tab) can create text and shapes directly in the browser. It also has an HTML API; output behavior depends on the method and options you use.
wkhtmltopdf(opens in a new tab) uses an older Qt WebKit rendering stack, and its repository is archived. Existing applications may still use it, but check its maintenance status and CSS limitations before choosing it for new work.
FAQ
Include the library, select a rendered element, and call html2pdf().set(options).from(element).save(). Run it after the content and assets are ready, and handle the returned promise.
Its normal HTML export renders content to an image and places that image in a PDF. Choose a text-producing method when selection, search, or extraction is required.
Use the pagebreak.before option with a selector for the element that should start a new page. You can also use supported CSS break rules and the legacy page-break class.
Check that images have loaded and that their server permits cross-origin access. The useCORS option requests permitted cross-origin images; it doesn’t override CORS restrictions.
Yes. Reference the element with a React ref and import the library inside a browser event handler. A static import can evaluate browser-only code during server rendering.
The standard HTML export captures the form’s appearance as an image. Use a PDF form generation API when users must interact with fields after downloading the document.
Evaluate Nutrient when you need server-side conversion, text output, or a documented PDF form generation workflow. Choose the cloud API or Document Engine according to where you need processing to run.