Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
html2canvas

How to Generate Multiple PDF Pages from Dynamic HTML with jsPDF and html2canvas

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To generate a multi-page PDF, render the fully populated HTML into one canvas, scale that canvas to the printable width of a jsPDF page, then place it again on new pages with a negative vertical offset. Calling addPage() by itself does not paginate HTML. The complete browser workflow below waits for data, fonts, and images, handles margins and aspect ratio, adds optional page breaks, and shows how to avoid browser canvas limits on very long reports.

What the workflow actually does

html2canvas paints a DOM element as a raster canvas. jsPDF then receives that image; it does not understand your document’s HTML flow. Pagination is therefore a mapping problem:

  1. Wait for dynamic content, web fonts, and images.
  2. Capture the report element with html2canvas.
  3. Create a jsPDF document with a known unit, paper size, and orientation.
  4. Calculate the printable width after margins and scale the canvas proportionally.
  5. Insert the first image with addImage().
  6. For every additional page, call addPage() and insert the same image at a negative vertical offset, or insert cropped canvas slices.
  7. Add headers, footers, or page numbers, then save the file.

The canvas approach produces a visual PDF. Text is generally not selectable or searchable because it is embedded as pixels rather than PDF text.

Complete browser implementation

HTML and CSS for a dynamic report

Keep the report in a dedicated element and mark controls that should not appear in the capture. The example uses a deliberate break before the second section and keeps cards together where possible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
MixPad Free Multitrack Recording Studio and Music Mixing Software [Download]
  • Create a mix using audio, music and voice tracks and recordings.
  • Customize your tracks with amazing effects and helpful editing tools.
  • Use tools like the Beat Maker and Midi Creator.
  • Work efficiently by using Bookmarks and tools like Effect Chain, which allow you to apply multiple effects at a time
  • Use one of the many other NCH multimedia applications that are integrated with MixPad.
<button id="export" type="button">Export PDF</button>
<main id="report">
  <h1>Quarterly report</h1>
  <section class="card">
    <h2>Summary</h2>
    <p id="summary">Loading…</p>
  </section>
  <div class="pdf-break" aria-hidden="true"></div>
  <section class="card">
    <h2>Details</h2>
    <div id="rows"></div>
  </section>
</main>

<style>
  .card { break-inside: avoid; page-break-inside: avoid; }
  .pdf-break { break-before: page; page-break-before: always; }
  [data-html2canvas-ignore="true"] { display: none !important; }
</style>

For content that must stay together, use break-inside: avoid (and the legacy page-break-inside property for older tooling). An explicit break element is useful when a new section should always begin on a fresh page. These rules express intent; a single raster canvas can still split content if it crosses a page boundary.

Capture, scale, and paginate

Load jsPDF and html2canvas before this script (for example, from your chosen package build), then adapt the data-loading function to your application.

async function loadReportData() {
  const response = await fetch('/api/report');
  if (!response.ok) throw new Error(`Report request failed: ${response.status}`);
  return response.json();
}

function renderReport(data) {
  document.querySelector('#summary').textContent = data.summary;
  document.querySelector('#rows').innerHTML = data.items.map(item =>
    `<article class="card"><h3>${escapeHtml(item.title)}</h3>` +
    `<p>${escapeHtml(item.description)}</p></article>`
  ).join('');
}

function escapeHtml(value) {
  return String(value).replace(/[&<>"']/g, character => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;',
    '"': '&quot;', "'": '&#39;'
  }[character]));
}

async function exportPdf() {
  const element = document.querySelector('#report');
  if (!element) throw new Error('The #report element was not found');

  const data = await loadReportData();
  renderReport(data);

  // Wait for web fonts and one layout/paint turn after dynamic rendering.
  if (document.fonts?.ready) await document.fonts.ready;
  await new Promise(requestAnimationFrame);

  const canvas = await html2canvas(element, {
    backgroundColor: '#fff',
    useCORS: true,
    windowWidth: element.scrollWidth,
    windowHeight: element.scrollHeight,
    scale: window.devicePixelRatio || 1,
    logging: false
  });

  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({
    unit: 'mm',
    format: 'a4',
    orientation: 'portrait'
  });
  const margin = 10;
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();
  const printableWidth = pageWidth - margin * 2;
  const printableHeight = pageHeight - margin * 2;
  const renderedHeight = canvas.height * printableWidth / canvas.width;
  const imageData = canvas.toDataURL('image/png');

  let remaining = renderedHeight;
  let offset = 0;
  pdf.addImage(imageData, 'PNG', margin, margin, printableWidth, renderedHeight);
  remaining -= printableHeight;

  while (remaining > 0) {
    offset -= printableHeight;
    pdf.addPage();
    pdf.addImage(imageData, 'PNG', margin, margin + offset,
      printableWidth, renderedHeight);
    remaining -= printableHeight;
  }

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

document.querySelector('#export').addEventListener('click', () => {
  exportPdf().catch(error => {
    console.error(error);
    alert('The PDF could not be generated. Check the console for details.');
  });
});

The width conversion is the key calculation: renderedHeight = canvas.height * printableWidth / canvas.width. It preserves the aspect ratio while converting canvas pixels into the jsPDF unit (millimetres here). Every page uses the same image width and the same vertical step, so the slices line up without stretching.

Headers, footers, and page numbers

Because the report image occupies the page, add repeating furniture after the image is placed. A simple footer can be added inside the pagination loop:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function addFooter(pdf, pageNumber, pageCount) {
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();
  pdf.setFontSize(9);
  pdf.text(`Page ${pageNumber} of ${pageCount}`,
    pageWidth / 2, pageHeight - 5, { align: 'center' });
}

To use it, first determine the number of pages from the loop (or collect page numbers), then call addFooter on each page. Keep footer coordinates inside the margin and reserve enough bottom space so the footer does not cover report content.

Preventing cut-off output on long reports

Match capture dimensions to the scrolled element

Passing element.scrollWidth and element.scrollHeight as windowWidth and windowHeight prevents a viewport-sized capture from stopping at the visible fold. This is especially important for reports whose content is populated after the initial page load.

Segment very tall canvases

Browsers impose finite maximum canvas dimensions. A very long report can therefore be clipped before jsPDF sees it. Capture the report in sections, or crop a completed canvas into page-sized pieces before calling addImage. The following helper creates a separate canvas for each page-sized slice in source pixels:

function canvasSlices(sourceCanvas, sliceHeight) {
  const slices = [];
  for (let top = 0; top < sourceCanvas.height; top += sliceHeight) {
    const height = Math.min(sliceHeight, sourceCanvas.height - top);
    const slice = document.createElement('canvas');
    slice.width = sourceCanvas.width;
    slice.height = height;
    const context = slice.getContext('2d');
    context.drawImage(
      sourceCanvas,
      0, top, sourceCanvas.width, height,
      0, 0, sourceCanvas.width, height
    );
    slices.push(slice);
  }
  return slices;
}

Choose sliceHeight from the printable page height after converting it to the canvas scale. In practice, segmenting the DOM into several smaller captures is safer than trying to create one enormous canvas, because each capture also uses less memory.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Controlling page breaks in HTML

CSS breaks and the html2pdf.js alternative

If you need a higher-level wrapper, html2pdf.js converts a webpage or element into a printable PDF client-side using html2canvas and jsPDF. Its documentation supports break-before, break-after, and break-inside, as well as the legacy html2pdf__page-break class. Its avoid-all mode attempts to keep elements together.

Direct jsPDF plus html2canvas gives you explicit control over capture timing, image placement, slicing, and custom page furniture. html2pdf.js reduces setup but leaves less of the pagination algorithm in your code. Neither approach changes the browser security rules for cross-origin images or iframes, and both inherit the raster nature of a canvas capture.

Concern Direct jsPDF + html2canvas html2pdf.js
Pagination control Manual offsets or explicit canvas slices Built-in break rules and an avoid-all mode
Setup More code, precise control Higher-level wrapper, less pagination code
Searchable text Canvas output is rasterized Canvas-based output is also generally rasterized
Cross-origin assets Requires CORS headers or a same-origin proxy Subject to the same browser restrictions
Very tall documents Use segmented captures or cropped slices Still constrained by browser canvas limits
Runtime and bundle cost Depends on the builds and options you include Adds a wrapper around the same rendering stack

Images, fonts, iframes, and visual fidelity

External images and CORS

Set useCORS: true only when the image server sends an appropriate Access-Control-Allow-Origin header. Otherwise, the canvas can become tainted and its pixels cannot be read with toDataURL. If you control neither server, proxy the image through your own origin and use the proxied URL in the report.

Cross-origin iframes

html2canvas cannot read an iframe’s contentDocument across origins because of browser security restrictions. Render the needed data in the parent page, host the frame content on the same origin, or capture that content separately with a server-side browser.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
DeskFX Free Audio Effects & Audio Enhancer Software [PC Download]
  • Transform audio playing via your speakers and headphones
  • Improve sound quality by adjusting it with effects
  • Take control over the sound playing through audio hardware

Sharpness versus memory

A higher scale improves small text and fine lines, but it multiplies canvas pixels and memory use. window.devicePixelRatio is a practical default; for large reports, use a controlled value and test on the lowest-memory devices you support. If the result is blurry, increase scale before changing the PDF dimensions.

Hide controls and transient UI

Add data-html2canvas-ignore="true" to buttons, loading indicators, or interactive controls that should not be printed. Remove spinners and skeletons only after the real data has rendered; otherwise the screenshot faithfully captures the loading state.

Troubleshooting checklist

Only the first page appears

  • Cause: the code rendered the image once and never added subsequent pages.
  • Fix: subtract printableHeight from the rendered image height, call addPage() while the remainder is positive, and use a negative vertical offset for each additional insertion.

The bottom of the report is missing

  • Cause: capture dimensions matched the viewport instead of the element’s scroll dimensions, or the browser hit its canvas maximum.
  • Fix: pass scrollWidth/scrollHeight, wait for dynamic rendering, and switch to segmented captures or slices for very tall documents.

Images are blank or the export throws a security error

  • Cause: an image is cross-origin without a permitted CORS response.
  • Fix: configure the image host’s CORS header, proxy the asset through your origin, or remove it from the capture. useCORS cannot bypass browser policy.

An embedded application or iframe is empty

  • Cause: its document is on another origin.
  • Fix: expose the content on the same origin or render it separately; html2canvas cannot inspect a cross-origin iframe.

Text is fuzzy

  • Cause: the canvas was rendered at too low a scale.
  • Fix: wait for document.fonts.ready, raise scale carefully, and watch memory usage.

Cards or headings split awkwardly

  • Cause: CSS break rules are hints, while the final PDF is a continuous raster image.
  • Fix: apply break-inside: avoid, insert explicit break elements, or capture logical sections separately when an exact boundary matters.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When a raster PDF is the wrong output

If readers must search, copy, select, or have assistive technology interpret the text, do not rely solely on a screenshot canvas. Build the PDF with jsPDF text APIs or choose a DOM-to-PDF engine that preserves text structure. That route requires separate handling for fonts, wrapping, tables, and images, but it avoids the fundamental limitation of a raster page.

Or skip the browser setup

For a URL that is already rendered, ScreenshotNeo provides a single screenshot or PDF request without wiring a headless browser into your application. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" 
  -d access_key=YOUR_API_KEY 
  --data-urlencode url=https://example.com/report 
  -o report.webp

See the ScreenshotNeo API documentation for parameters. The same endpoint can return PNG, JPEG, WebP, or PDF; options include full-page lazy-image loading, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click and wait actions, hidden selectors, blocked requests or resource types, custom headers/cookies/user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, a chosen cache TTL, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

Best Value
MixPad Multitrack Recording Software for Sound Mixing and Music Production Free [Mac Download]
  • Mix an audio, music and voice tracks
  • Record single or multiple tracks simultaneously
  • Intuitive tools to split, trim, join, and many other editing features
  • Loaded with audio effects including EQ, compression, reverb, and more.
  • Load an audio file and export to all popular audio formats from studio quality wav to high compression formats
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/report"},
    timeout=90
)
r.raise_for_status()
open("report.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/report'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('report.webp', buffer));

ScreenshotNeo includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

Frequently Asked Questions

Can I switch from PNG to JPEG or WebP in the browser implementation?

Yes. Pass the matching image format and data URL to jsPDF, but choose JPEG for photographs and PNG when sharp text, transparency, or lossless edges matter.

How should I test pagination changes?

Use deterministic fixture data with short and very long strings, missing images, multiple font sizes, and enough rows to cross several page boundaries; then inspect the first, middle, and final pages at the smallest supported device scale.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What happens if dynamic content keeps changing during capture?

Export only after the application signals that fetching, image loading, and layout updates are complete; otherwise html2canvas records whichever state happened to be painted at capture time.

Quick Recap

Bestseller No. 1
MixPad Free Multitrack Recording Studio and Music Mixing Software [Download]
MixPad Free Multitrack Recording Studio and Music Mixing Software [Download]
Create a mix using audio, music and voice tracks and recordings.; Customize your tracks with amazing effects and helpful editing tools.
Bestseller No. 4
DeskFX Free Audio Effects & Audio Enhancer Software [PC Download]
DeskFX Free Audio Effects & Audio Enhancer Software [PC Download]
Transform audio playing via your speakers and headphones; Improve sound quality by adjusting it with effects
Bestseller No. 5
MixPad Multitrack Recording Software for Sound Mixing and Music Production Free [Mac Download]
MixPad Multitrack Recording Software for Sound Mixing and Music Production Free [Mac Download]
Mix an audio, music and voice tracks; Record single or multiple tracks simultaneously; Intuitive tools to split, trim, join, and many other editing features

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.