Export HTML to PDF in the Browser with jsPDF

Blog / Django · June 29, 2019 · Updated June 10, 2026 · 7 min read
Export HTML to PDF in the Browser with jsPDF

To export an HTML web page to PDF in the browser with jsPDF, install jsPDF v3, render the target DOM element to a canvas with html2canvas, then add that image to a jsPDF document and call doc.save() — or skip the manual step and pass the element straight to doc.html(element, { callback }). Both run entirely client-side, with no server round-trip.

The catch worth knowing up front: jsPDF + html2canvas rasterizes your DOM into an image, so the resulting text is not selectable or searchable and complex CSS can drift. When you need pixel-perfect, multi-page, fully selectable PDFs, rendering on the server (WeasyPrint or Puppeteer) is usually the better tool.

Key takeaways

  • jsPDF v3 is ESM-first: import { jsPDF } from 'jspdf' after npm install jspdf (or load it from a CDN).
  • The classic flow is html2canvas → canvas.toDataURL()doc.addImage()doc.save(); you slice the canvas yourself for multi-page output.
  • doc.html(element, { callback, autoPaging: 'text' }) wraps html2canvas and handles page breaks for you.
  • Anything image-based (html2canvas, doc.html()) produces non-selectable text and can mis-render advanced CSS.
  • For pixel-perfect, selectable, multi-page documents at scale, render server-side with WeasyPrint or Puppeteer.
  • For a quick, user-driven export, a print stylesheet plus window.print() ("Save as PDF") often beats shipping a PDF library.

How does jsPDF export an HTML page to PDF?

jsPDF is a pure client-side library: it builds the PDF file in JavaScript and triggers a download, with no server involved. On its own it draws vector primitives — doc.text(), lines, shapes — into a PDF.

To capture an existing HTML layout, jsPDF leans on html2canvas, which paints a DOM node onto an HTML <canvas>. You then embed that canvas as an image in the PDF. That is why the output is faithful to what is on screen but image-based: the PDF contains a picture of your page, not its underlying text. Keep that distinction in mind — it drives every decision below.

Which HTML-to-PDF approach should you use?

Four approaches dominate, and they trade fidelity for convenience differently:

Approach Fidelity Selectable text Multi-page Works offline Best for
jsPDF + html2canvas Medium (image of the DOM) No Manual slicing Yes (in browser) Quick client-side snapshot of one element
jsPDF .html() Medium (wraps html2canvas) Limited Automatic (autoPaging) Yes (in browser) Styled HTML to PDF with auto page breaks
window.print() + print CSS High (browser engine) Yes Automatic Yes (in browser) User-initiated "Save as PDF", invoices styled for print
Server-side (WeasyPrint / Puppeteer) Highest (true layout) Yes Automatic N/A (on server) Pixel-perfect, repeatable, accessible documents at scale

If selectable text or exact layout matters, the bottom two rows win. If you just need a "download what I see" button, the top two are quickest.

How do I install and import jsPDF v3?

jsPDF v3 ships as an ES module. Install it alongside html2canvas (used for the DOM-capture route) and import the named jsPDF export — the old default-export require('jspdf') style from pre-v2 tutorials is gone:

// npm install jspdf html2canvas
import { jsPDF } from 'jspdf';
import html2canvas from 'html2canvas';

const doc = new jsPDF();
doc.text('Hello from jsPDF v3', 10, 10);
doc.save('hello.pdf');

No build step? Load the UMD builds from a CDN; the bundle attaches jsPDF to window.jspdf, so destructure jsPDF off it:

<!-- jsPDF v3 + html2canvas via CDN (UMD builds) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/3.0.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>
  // The UMD build exposes the constructor on window.jspdf
  const { jsPDF } = window.jspdf;
  const doc = new jsPDF();
  doc.text('Hello from jsPDF v3', 10, 10);
  doc.save('hello.pdf');
</script>

How do I convert a DOM element to a multi-page PDF?

This is the most common pattern: render an element to a high-resolution canvas with html2canvas, then slice that one tall image across A4 pages. The trick is to compute the image height from the canvas aspect ratio, then shift the same image up by one page height for each new page until nothing is left.

import { jsPDF } from 'jspdf';
import html2canvas from 'html2canvas';

async function exportToPdf(elementId) {
  const element = document.getElementById(elementId);

  // Render the DOM node to a high-resolution canvas.
  const canvas = await html2canvas(element, {
    scale: 2,      // 2x for sharper text on the rasterized image
    useCORS: true, // allow cross-origin images to be captured
  });

  const imgData = canvas.toDataURL('image/png');
  const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });

  const pageWidth = pdf.internal.pageSize.getWidth();   // 210mm for A4
  const pageHeight = pdf.internal.pageSize.getHeight(); // 297mm for A4

  // Scale the image to the page width and derive its full height.
  const imgWidth = pageWidth;
  const imgHeight = (canvas.height * imgWidth) / canvas.width;

  let heightLeft = imgHeight;
  let position = 0;

  // First page.
  pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
  heightLeft -= pageHeight;

  // Extra pages: shift the same tall image up by one page each time.
  while (heightLeft > 0) {
    position -= pageHeight;
    pdf.addPage();
    pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
    heightLeft -= pageHeight;
  }

  pdf.save('page.pdf');
}

exportToPdf('content');

A few things that trip people up with the rasterized approaches:

  • Blank or cut-off pages? Make sure imgHeight is computed from the canvas aspect ratio (as above), not hard-coded.
  • Blurry text? Raise html2canvas scale to 2 or 3 for retina-sharp output, at the cost of a larger file.
  • Force a clean break by giving block elements style="break-inside: avoid;" so html2canvas keeps them whole where possible.
  • Modern CSS colors like oklch() can break classic html2canvas; the html2canvas-pro fork handles them and can be passed to jsPDF via the html2canvas option.
  • Cross-origin images render blank unless the source sends CORS headers and you set useCORS: true.

How do I use the jsPDF doc.html() method?

If you would rather not manage canvases and page slicing yourself, doc.html() wraps html2canvas and adds automatic paging. Pass the element, a callback that receives the finished document, and a few sizing options. Set autoPaging: 'text' so lines are not sliced in half across page breaks:

import { jsPDF } from 'jspdf';

async function exportWithDocHtml() {
  const element = document.getElementById('content');
  const doc = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });

  await doc.html(element, {
    callback: (doc) => doc.save('page.pdf'),
    x: 0,
    y: 0,
    width: 595,         // A4 width in pt; scales the element to fit
    windowWidth: 1024,  // CSS pixel width used while rendering the DOM
    autoPaging: 'text', // avoid slicing lines of text across page breaks
    margin: [24, 24, 24, 24],
  });
}

exportWithDocHtml();

When should you render the PDF on the server instead?

Everything above happens in the browser, which is great for a quick "download this view" button. But the trade-off is real: html2canvas and doc.html() both photograph the DOM, so the text in the PDF is not selectable, not searchable, and not accessible to screen readers — and intricate CSS (flex/grid edge cases, custom fonts, running headers and footers) can drift from what you see on screen.

When the PDF is the product — invoices, contracts, statements, reports that must look identical every time and stay selectable — render it on the server from HTML and CSS instead. WeasyPrint turns HTML and CSS into true, text-selectable PDFs from a Django backend, and headless Chrome (Puppeteer) does the same using the browser's own layout engine. You get real pagination, repeatable output, and consistent results regardless of the user's device.

A common pattern is to keep the front end light: bundle and minify the styles that feed both the on-screen view and the PDF — django-webpacker is a handy tool for bundling CSS and JS files — then hand the final HTML to the server for rendering.

If you are weighing a client-side export against a server-rendered pipeline, our web development team builds both, and our Django development services cover the WeasyPrint server-side path end to end.

Frequently Asked Questions

Can jsPDF export an entire HTML page to PDF?

Yes. Pass document.body or a wrapper element to html2canvas or to doc.html() and jsPDF renders the whole page. For long pages, use the multi-page loop shown above or set autoPaging: 'text' on doc.html() so content flows across A4 pages instead of being clipped to one.

Is the text in a jsPDF + html2canvas PDF selectable?

No. html2canvas rasterizes the DOM into an image and jsPDF embeds that image, so the result behaves like a screenshot — you cannot select, copy, or search the text, and screen readers cannot read it. If selectable text matters, use doc.text() to place real text, a print stylesheet with window.print(), or server-side rendering with WeasyPrint or Puppeteer.

What is the difference between jsPDF .html() and html2canvas plus addImage?

doc.html() is a convenience wrapper: it calls html2canvas for you and handles page breaks via autoPaging. The manual html2canvas → addImage route gives you full control over scaling, image format, compression, and exactly where each page is sliced. Both are image-based, so neither produces selectable text.

How do I fix blank or cut-off pages in a multi-page jsPDF export?

Blank or clipped pages usually mean the image height was not split correctly. Compute the image height from the canvas aspect ratio (canvas.height * pageWidth / canvas.width), then loop: add the image, subtract one page height, shift the y-position up by a page, and call addPage() until no height remains. Avoid hard-coded heights.

Which jsPDF version should I use in 2026, and is it free?

Use jsPDF v3.x — it is ESM-first (import { jsPDF } from 'jspdf'), actively maintained, and replaces the long-removed fromHTML and addHTML APIs from older tutorials with the modern .html() method. jsPDF is open source under the MIT license, so it is free to use in commercial projects.

When is server-side PDF rendering better than jsPDF in the browser?

Choose server-side rendering (WeasyPrint, Puppeteer, or similar) when you need pixel-perfect, repeatable documents, selectable and accessible text, reliable multi-page layout with headers and footers, or generation without a user's browser — for example invoices, contracts, and batch reports. Browser-side jsPDF is best for quick, ad-hoc "download what I see" exports.

Share this article