How to Convert HTML to PDF Client-Side with JavaScript
Convert a DOM element to a downloadable PDF in the browser with html2pdf.js, control page breaks, fix CORS issues, and choose the right PDF approach.
Use html2pdf.js for most client-side HTML-to-PDF downloads. It combines html2canvas, which reconstructs the selected DOM in a canvas, with jsPDF, which places that rendered output into a PDF. This works well for invoices, reports, receipts, cards, and other controlled layouts where visual output matters more than editable PDF structure.
The complete flow is:
- Wait until fonts, images, charts, and asynchronous data are ready.
- Select the element to export.
- Set the paper size, orientation, margins, image quality, and page-break rules.
- Call
html2pdf().set(options).from(element).save().
1. Minimal working example
Save this as an HTML file and open it in an evergreen browser:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML to PDF</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
#invoice { max-width: 760px; margin: auto; }
.screen-only { margin-bottom: 1rem; }
.report-section { break-inside: avoid; }
@media print {
.screen-only { display: none; }
}
</style>
</head>
<body>
<button class="screen-only" id="download-pdf">Download PDF</button>
<article id="invoice">
<h1>Invoice</h1>
<p>Content to export.</p>
<section class="report-section">
<h2>Line items</h2>
<p>A section that should stay together when possible.</p>
</section>
</article>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
document.querySelector('#download-pdf').addEventListener('click', () => {
const element = document.querySelector('#invoice');
const options = {
margin: 0.5,
filename: 'invoice.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' },
pagebreak: { mode: ['css', 'legacy'] }
};
html2pdf().set(options).from(element).save();
});
</script>
</body>
</html>
The documented shorthand html2pdf(document.body) exports the whole body. For a specific section, the worker form gives you control over the source element and options.
2. Install and load html2pdf.js
CDN in a browser page
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
Bundled application
Install the package with your package manager, then import it in your application entry point:
npm install html2pdf.js
import html2pdf from 'html2pdf.js';
Use the same worker chain after the import. The exact bundler configuration can vary, so verify that the library runs in the browser rather than during server-side rendering.
3. Export a div, article, or component
const element = document.querySelector('#report');
if (!element) throw new Error('Export element was not found');
html2pdf()
.set({ filename: 'report.pdf' })
.from(element)
.save();
Keep the export root separate from controls such as buttons and menus. Add a screen-only class to anything that should disappear in the PDF and hide it in the export stylesheet.
4. Configure paper, margins, images, and orientation
| Option | Purpose | Example |
|---|---|---|
filename |
Downloaded file name | 'invoice.pdf' |
margin |
Page margin in the jsPDF unit | 0.5 |
image.type |
Raster format used inside the PDF | 'jpeg' or 'png' |
image.quality |
JPEG quality from 0 to 1 | 0.95 |
html2canvas.scale |
Render scale and visual sharpness | 2 |
html2canvas.useCORS |
Request CORS-enabled images | true |
jsPDF.unit |
Measurement unit | 'in', 'mm', or 'pt' |
jsPDF.format |
Paper size | 'letter', 'a4' |
jsPDF.orientation |
Page direction | 'portrait' or 'landscape' |
const options = {
margin: [0.5, 0.5, 0.5, 0.5],
filename: 'quarterly-report.pdf',
image: { type: 'png', quality: 1 },
html2canvas: {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff'
},
jsPDF: {
unit: 'in',
format: 'a4',
orientation: 'landscape'
},
pagebreak: { mode: ['css', 'legacy'] }
};
html2pdf().set(options).from(document.querySelector('#report')).save();
PNG preserves sharp edges and transparency better, while JPEG can reduce file size for photo-heavy content. Higher canvas scale improves detail but increases memory use and rendering time. Choose a fixed export width so responsive breakpoints do not change the document between screen sizes.
5. Control page breaks
html2pdf.js supports CSS and legacy page-break modes. Add break rules to headings, cards, and table sections that should remain together:
.report-section {
break-inside: avoid;
page-break-inside: avoid;
}
.page-break-before {
break-before: page;
page-break-before: always;
}
.page-break-after {
break-after: page;
page-break-after: always;
}
<section class="report-section">...</section>
<div class="page-break-before"></div>
<section>...</section>
The explicit html2pdf__page-break class is also available for a forced break:
<div class="html2pdf__page-break"></div>
For print-specific layout rules, use a print media block:
@media print {
.screen-only { display: none; }
.report-section { break-inside: avoid; }
}
Long tables need special care. Test them at the target paper size, keep rows from becoming taller than a page, and decide whether a repeated header is required. Canvas-based pagination can split content in ways that differ from the browser print preview.
6. Wait for fonts, images, charts, and data
Export only after the DOM is stable. A click handler can wait for images and fonts:
async function waitForAssets(root) {
if (document.fonts?.ready) await document.fonts.ready;
const images = [...root.querySelectorAll('img')];
await Promise.all(images.map(img => {
if (img.complete) return Promise.resolve();
return new Promise(resolve => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
}
async function downloadReport() {
const element = document.querySelector('#report');
await waitForAssets(element);
await html2pdf()
.set({ filename: 'report.pdf', html2canvas: { scale: 2, useCORS: true } })
.from(element)
.save();
}
Charts rendered by a framework should be fully painted before this function runs. If data arrives over the network, call the exporter after the request and rendering state complete.
7. Images, fonts, iframes, and CSS limitations
html2canvas does not take a literal screenshot. It reconstructs a representation from DOM information and only renders CSS properties it understands. That means a browser view and the PDF can differ for unsupported CSS.
- Serve images from the same origin, or configure the image host with permissive CORS headers.
useCORS: truerequests CORS access but cannot override missing server headers or browser security policy.- Cross-origin iframes cannot be traversed because their document is inaccessible. Same-origin iframes are supported.
- Plugin content and browser-only surfaces are not reliable inputs to the html2canvas path.
- Simplify the export stylesheet when exact output matters; avoid depending on CSS properties the renderer does not support.
8. A reusable export function
export async function exportElementToPdf(element, {
filename = 'document.pdf',
format = 'a4',
orientation = 'portrait',
margin = 0.5,
scale = 2,
imageType = 'jpeg',
quality = 0.95
} = {}) {
if (!element) throw new TypeError('A DOM element is required');
if (document.fonts?.ready) await document.fonts.ready;
const images = [...element.querySelectorAll('img')];
await Promise.all(images.map(img => img.complete
? Promise.resolve()
: new Promise(resolve => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
})));
return html2pdf().set({
margin,
filename,
image: { type: imageType, quality },
html2canvas: { scale, useCORS: true },
jsPDF: { unit: 'in', format, orientation },
pagebreak: { mode: ['css', 'legacy'] }
}).from(element).save();
}
document.querySelector('#download-pdf').addEventListener('click', () =>
exportElementToPdf(document.querySelector('#report'), {
filename: 'report.pdf',
format: 'letter'
})
);
9. Choosing between html2pdf.js, jsPDF, and pdf-lib
| Need | Best fit | Reason |
|---|---|---|
| Export an existing DOM layout | html2pdf.js | Combines DOM reconstruction with PDF output and page-break controls. |
| Draw a PDF from primitives | jsPDF | Useful when you control every text, image, and coordinate. |
| Create, merge, split, fill, or edit PDF objects | pdf-lib | Pure JavaScript and usable in browsers, Node, Deno, and React Native. |
Use pdf-lib when the document must contain structured PDF objects, embedded fonts, form fields, or merged pages. It is not a drop-in replacement for reproducing arbitrary HTML and CSS.
10. Troubleshooting
Images are missing or the canvas is tainted
Cause: The image is cross-origin and the server does not grant CORS access. Fix: Serve it from the same origin, configure the image server’s CORS headers, and keep useCORS: true. You cannot fix a denied browser request from JavaScript alone.
CSS looks different from the page
Cause: html2canvas reconstructs supported DOM and CSS rather than capturing the browser compositor. Fix: simplify the export stylesheet, use explicit dimensions and colors, and avoid unsupported properties in the export area.
Text or fonts look wrong
Cause: The export started before web fonts loaded. Fix: await document.fonts.ready and make sure the font files are reachable with the correct CORS policy.
Content is cut off at a page boundary
Cause: A component is taller than the remaining page area or the renderer cannot honor a break rule. Fix: add break-inside: avoid, insert an explicit page break before the component, reduce its height, and test at the target paper size.
Cross-origin iframe content is blank
Cause: Browser same-origin rules prevent html2canvas from reading the iframe document. Fix: render the content in the parent page or use a same-origin iframe. A cross-origin iframe cannot be traversed by this client-side path.
The browser becomes slow or runs out of memory
Cause: Large DOM trees and high canvas scales create large bitmaps. Fix: export a focused element, reduce scale, use JPEG for photographic content, remove unnecessary off-screen elements, and split very long documents into smaller exports.
The downloaded file is empty
Cause: The selected element is missing, hidden, or has not received its data. Fix: verify the selector, wait for rendering, and export an element with non-zero dimensions.
11. Performance, reliability, privacy, and cost
- Performance: Rendering cost grows with DOM size, pixel dimensions, image count, and canvas scale. Export only what the user needs.
- Reliability: Keep a fixed export width, explicit paper settings, stable fonts, and deterministic data. Test invoices, long tables, charts, and empty states separately.
- Privacy: Client-side conversion keeps the document in the browser, subject to the third-party assets your page loads.
- Cost: The libraries run in the user’s browser, so there is no conversion API request charge. Browser memory and user device limits still apply.
- Accessibility: Because the html2canvas path places a rendered image into the PDF, text may not remain as structured, selectable PDF text. Choose a PDF-object library when that matters.
12. Or skip the browser setup
If you need a reliable screenshot or PDF of a URL instead of exporting an already-rendered local DOM, ScreenshotNeo provides a single API request. Its capture flow accepts cookie and consent banners as a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also offers an MCP server for AI agents with take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
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}`);
ScreenshotNeo includes full-page capture, PDF output, device presets, custom viewports, waiting rules, custom CSS and JavaScript, request blocking, headers and cookies, caching, signed links, async jobs, bulk capture, and a usage API. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
13. FAQ
Can I convert only one div?
Yes. Pass that element to .from(element) instead of exporting document.body.
Does html2pdf.js create a true vector PDF?
The common html2canvas pipeline rasterizes the rendered DOM into the PDF. Use a PDF-object library when selectable text, forms, or structured PDF editing is required.
Can I convert HTML to PDF without a server?
Yes. html2pdf.js runs in the browser. It still depends on browser security rules for images, fonts, and iframes.
How do I export an A4 landscape document?
Set jsPDF: { unit: 'in', format: 'a4', orientation: 'landscape' } and test page breaks at that paper size.
Should I use pdf-lib for an HTML page?
Use html2pdf.js for DOM-oriented visual export. Use pdf-lib for creating or editing PDF objects rather than reproducing arbitrary HTML and CSS.


