ScreenshotNeo

BlogHTML to image & PDF

How to Render HTML Across Multiple jsPDF Pages Correctly

Learn how jsPDF html() pagination works, how to prevent clipped content, and how to build reliable multi-page PDFs from browser HTML.

By the ScreenshotNeo team30 September 20269 min read

How to Render HTML Across Multiple jsPDF Pages Correctly

Rendering a long HTML document into a PDF is more than calling doc.save(). The conversion must finish before you save, the installed jsPDF version must support the options you use, and the HTML renderer must be allowed to split content at page boundaries. If any of those assumptions is wrong, output may be clipped, blank, duplicated, or split in the middle of important blocks.

Short answer: use html() with version-aware automatic paging

Use the jsPDF HTML plugin and call doc.html(element, options). The plugin uses html2canvas for browser rendering and, when you pass an HTML string, may also use DOMPurify. Its implementation exposes paging through pdf.context2d.autoPaging. Check the exact jsPDF package and plugin version installed in your application before choosing a mode or relying on a CSS behavior.

Save the document only from the completion callback (or the promise flow supported by your installed version). addPage() and setPage() control explicitly created PDF pages; they do not define how a tall HTML render is automatically split.

1. Confirm the jsPDF version and dependencies

Start by checking the package that is actually bundled, rather than copying options from the rolling documentation for a different release.

Automatic paging flows one rendered HTML document across multiple PDF pages.
Automatic paging flows one rendered HTML document across multiple PDF pages.
npm ls jspdf html2canvas dompurify
# or
npm view jspdf version

The HTML route depends on jsPDF and html2canvas. If you pass a string instead of a DOM element, include the sanitization dependency expected by your version. Keep jsPDF, html2canvas, and DOMPurify versions pinned together in production so a dependency update does not silently change pagination.

2. A complete browser example

This example renders an on-page element, waits for conversion, and then saves the PDF. It deliberately keeps the source element in the document so fonts, computed styles, images, and layout have a real browser context.

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

// Make the plugin available to jsPDF in builds that require explicit wiring.
// In many current bundles, importing jspdf is sufficient.
window.html2canvas = html2canvas;

async function exportInvoice() {
  const source = document.querySelector('#invoice');
  if (!source) throw new Error('Missing #invoice element');

  const pdf = new jsPDF({
    orientation: 'portrait',
    unit: 'mm',
    format: 'a4',
    putOnlyUsedFonts: true
  });

  const options = {
    margin: [12, 12, 14, 12],
    autoPaging: 'text',
    html2canvas: {
      scale: 2,
      useCORS: true,
      backgroundColor: '#ffffff'
    },
    callback: function (finishedPdf) {
      finishedPdf.save('invoice.pdf');
    }
  };

  pdf.html(source, options);
}

document.querySelector('#export').addEventListener('click', exportInvoice);

The callback is the important part: conversion, image loading, canvas rasterization, and page placement happen asynchronously. Any call to save(), page counting, or follow-up drawing before that callback can produce an incomplete file.

3. Understand the page and size options

Option What it controls Practical advice
format Paper size such as 'a4' or 'letter' Choose the destination standard before tuning CSS widths.
orientation 'portrait' or 'landscape' Landscape often prevents wide tables from being squeezed.
unit PDF coordinate unit, commonly 'mm', 'pt', or 'px' Use one unit consistently when calculating dimensions.
margin Outer content inset Use an array when top, right, bottom, and left margins differ.
autoPaging HTML flow across pages Confirm accepted values and defaults in the installed release.
html2canvas Rasterization options Set scale, CORS behavior, and background deliberately.
callback Completion notification Save and inspect only here.

The exact autoPaging modes and defaults are version-specific. The implementation exposes pdf.context2d.autoPaging; inspect the source or type definitions shipped with your version instead of assuming that an option documented for master exists in an older lockfile.

4. Make CSS paginate predictably

Automatic paging works on the rendered layout, not on your source file’s line breaks. A block can be split when its computed height crosses a page edge. Reduce surprises with these techniques:

  • Give the root content a stable width and avoid viewport-dependent widths such as 100vw.
  • Use normal document flow. Absolute positioning and large transforms can place pixels outside the renderer’s measured flow.
  • Keep headings with the following content by wrapping each heading and its first paragraph in a block and applying break-inside: avoid where your installed renderer honors it.
  • For tables, repeat the header row with display: table-header-group and avoid one giant row containing an entire page.
  • Do not depend on sticky, fixed, or interactive browser behavior; the PDF receives a rendered snapshot.
  • Set explicit image dimensions or reserve space so late image loads do not reflow an earlier page.
.pdf-root {
  width: 180mm;
  color: #111;
  background: #fff;
  font: 10pt/1.45 Arial, sans-serif;
}
.pdf-section {
  break-inside: avoid;
  page-break-inside: avoid;
}
table thead {
  display: table-header-group;
}
img {
  max-width: 100%;
  height: auto;
}

There is no universal CSS page-break recipe across all jsPDF releases. Verify the output with the exact DOM, CSS, browser, and dependency versions used by your application.

5. Automatic flow versus explicit pages

Automatic HTML flow

Use html() with automatic paging when your source is a report, invoice, article, or other normal-flow document. It requires less page bookkeeping and lets the renderer place content continuously. Its trade-off is less control at boundaries: a block near the bottom can still split, and behavior can change with fonts, images, or library versions.

Explicit page construction

Use document-level APIs when each page has a known structure, such as a cover, a summary, and a fixed chart page.

const pdf = new jsPDF({ format: 'a4', unit: 'mm' });
pdf.text('Cover', 20, 25);
pdf.addPage('a4', 'landscape');
pdf.text('Landscape chart page', 20, 25);
pdf.setPage(1);
pdf.text('Added after revisiting the cover', 20, 40);
pdf.save('structured.pdf');

addPage() creates and activates a page. setPage(n) selects an existing page. These APIs do not change how html() splits a tall element; mixing them into the middle of an HTML conversion can make the result difficult to reason about. Finish the HTML callback first, then add deliberately positioned content.

6. Fonts, images, and asynchronous content

  1. Wait for web fonts. Call await document.fonts.ready before starting conversion so fallback metrics do not change line wrapping.
  2. Use CORS-compatible images. Cross-origin images without suitable CORS headers may be omitted or taint the canvas. Host assets with an allowed origin or proxy them through your own origin.
  3. Freeze dynamic data. Render charts and timestamps before calling html(); a live animation can capture inconsistent frames.
  4. Reserve image space. Set width and height attributes or CSS dimensions to prevent layout shifts while images load.
  5. Check print colors. A transparent or dark page background may look different when rasterized. Set an explicit background in both CSS and html2canvas options.
await document.fonts.ready;
await Promise.all([...document.images].map(image => {
  if (image.complete) return Promise.resolve();
  return new Promise(resolve => {
    image.addEventListener('load', resolve, { once: true });
    image.addEventListener('error', resolve, { once: true });
  });
}));
// Call pdf.html(...) only after the waits above.

7. Debug clipped, blank, or incorrectly split output

Symptom Likely cause Fix
Only the first page appears Conversion was saved before completion, or the content is outside the measured element Save in callback; remove absolute positioning and verify the source element height.
Output is blank Element is hidden, has zero dimensions, or a canvas/image failed Render a visible element, inspect its bounding box, and test with plain text first.
Text is clipped at the right edge CSS width exceeds the PDF content width Set a fixed root width, reduce margins, or choose landscape format.
Images are missing CORS restrictions or images not loaded before capture Use same-origin/CORS-enabled assets and wait for document.images.
Heading is stranded at the bottom Renderer-specific break behavior Wrap heading plus opening content, apply break-inside: avoid, and verify your jsPDF version.
Pages differ after an upgrade Rolling plugin or html2canvas behavior changed Pin versions, record the installed version, and compare generated PDFs in CI.
Non-Latin text is missing Font does not contain the glyphs or was not embedded Embed a font supported by your jsPDF build and verify the font loads before conversion.

When diagnosing, reduce the document to one heading, one paragraph, and one image. Add tables, custom fonts, and complex CSS back one at a time. Inspect page count, page edges, continuation of paragraphs, image presence, and keep-together blocks rather than judging only the first page.

8. Performance, reliability, and cost considerations

  • Performance: html2canvas rasterizes pixels, so larger scale values increase memory and processing work. Use the lowest scale that meets your print requirement and avoid rendering hidden duplicate copies of the document.
  • Reliability: Keep the browser viewport, fonts, assets, and jsPDF versions stable. A small width change can reflow text and alter every later page.
  • Large documents: Split extremely long reports into logical sections if browser memory becomes a problem, then merge PDFs in a separate step. Measure page count and file size in your own workload; the supplied sources do not establish universal limits.
  • Security: Sanitize untrusted HTML before inserting it into the DOM. If passing a string, use the DOMPurify integration expected by your version, but still treat URLs, CSS, and event attributes as untrusted input.
  • Cost: jsPDF runs in your application. Your costs come from browser CPU, memory, and any infrastructure used to render or store the result, rather than a per-page jsPDF fee.
A capture service can remove obstructive overlays before rendering the final page.
A capture service can remove obstructive overlays before rendering the final page.

9. Test a multi-page export before shipping

  • Use a fixture long enough to produce at least three pages.
  • Place a heading, paragraph, table, image, and footer near page boundaries.
  • Test both slow and cached image loads.
  • Test the fonts and browser versions your users actually run.
  • Open the generated PDF and confirm page count, readable text, image presence, margins, and continuation order.
  • Record the jsPDF, html2canvas, and browser versions with the fixture result.

Or skip the browser setup

If you need a dependable screenshot or PDF from a URL rather than a browser-side HTML conversion, ScreenshotNeo provides a single request API. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools named take_screenshot, get_page_info, and capture_pdf.

For PDF output, options include paper size, margins, landscape mode, and page ranges. The service also supports full-page capture with lazy images loaded, custom CSS and JavaScript, selectors, waits, headers, cookies, user agents, authorization, timezone, geolocation, blocked resources, caching, signed links, asynchronous jobs, bulk capture, and a usage API. See the ScreenshotNeo API documentation for the current parameter names.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', bytes);

There are 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to try the API.

FAQ

Does addPage() fix a clipped HTML render?

No. It creates a document page; it does not control the HTML plugin’s internal flow. Configure and verify automatic paging in the version you installed.

Should I pass a DOM element or an HTML string?

A DOM element is usually easier to make deterministic because it already has computed styles, loaded fonts, and resolved assets. A string requires the sanitization and parsing path supported by your version.

Can I guarantee that a card never splits?

You can request keep-together behavior with CSS such as break-inside: avoid, but support varies with renderer and layout. Test the exact document and dependency versions.

Why does a PDF look different from the browser page?

The plugin renders through html2canvas and PDF coordinates. Browser-only effects, cross-origin assets, animations, and viewport-dependent CSS can therefore differ in the captured result.

When should I use a URL screenshot API instead?

Use one when the source is already a deployed URL, you need server-side or automated capture, or you do not want to maintain browser setup, asset loading, and pagination code.