ScreenshotNeo

BlogHTML to image & PDF

How to Save a Web Page as PDF with HTML2Canvas and jsPDF

Capture a DOM element with html2canvas, paginate it with jsPDF, and handle fonts, images, long pages, CORS, errors, and production limits.

By the ScreenshotNeo team30 September 20269 min read

How to Save a Web Page as PDF with HTML2Canvas and jsPDF

Use html2canvas to reconstruct a DOM element as a canvas, then place that image in a jsPDF document and save the file. The result is a visual PDF approximation of the rendered page. It is not a native browser screenshot, and it does not automatically create selectable, semantic text.

For a reliable export, capture the content container rather than the entire body, wait for images and fonts, choose a deliberate page size, preserve the image aspect ratio, and split long content into page-sized slices. Cross-origin images need CORS permission or a proxy, and very tall canvases can fail differently across browsers.

What html2canvas and jsPDF actually do

html2canvas walks the DOM and builds a canvas from the CSS and content it understands. The project documentation says: “The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.” Unsupported CSS, browser security rules, animations, and missing resources can therefore change the output.

html2canvas reconstructs the DOM as a canvas before jsPDF places it on PDF pages.
html2canvas reconstructs the DOM as a canvas before jsPDF places it on PDF pages.

jsPDF creates the PDF container. You choose orientation, units, and format, add the canvas as an image, add pages when necessary, and call save(). Its documented default is A4 portrait with millimetre units, but specifying these values makes your intent clear.

Requirement Recommended approach
Export one report or card Pass that element to html2canvas().
Preserve page proportions Calculate PDF width from the canvas aspect ratio.
Export a long report Slice the canvas into page-height sections or capture smaller sections.
Use remote images Serve them with suitable CORS headers or fetch them through a permitted proxy.
Support non-Latin text Embed a TTF font in jsPDF with the required glyphs.

Install the libraries

Install both packages with your project package manager:

npm install html2canvas jspdf

Then import them in a browser bundle:

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

If you are not using a bundler, load browser builds before your application code. Pin versions in production and verify the generated file in every browser you support.

Basic export: capture an element and save one PDF page

This complete example exports an element with the id invoice. The scale value improves detail on high-density screens; lower it if memory use is a problem.

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

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

  // Let layout, images, and web fonts settle before capturing.
  if (document.fonts?.ready) {
    await document.fonts.ready;
  }
  await Promise.all(
    [...element.querySelectorAll('img')].map((img) => {
      if (img.complete) return Promise.resolve();
      return new Promise((resolve) => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', resolve, { once: true });
      });
    })
  );

  const canvas = await html2canvas(element, {
    backgroundColor: '#ffffff',
    scale: Math.min(window.devicePixelRatio || 1, 2),
    useCORS: true,
    logging: false
  });

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

  const margin = 12;
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();
  const usableWidth = pageWidth - margin * 2;
  const imageHeight = (canvas.height * usableWidth) / canvas.width;
  const usableHeight = pageHeight - margin * 2;

  if (imageHeight <= usableHeight) {
    pdf.addImage(canvas, 'PNG', margin, margin, usableWidth, imageHeight);
  } else {
    throw new Error('Content is taller than one page; use the pagination example below.');
  }

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

document.querySelector('#save-pdf').addEventListener('click', saveElementAsPdf);

The canvas is converted directly by jsPDF. You can use 'JPEG' instead of 'PNG' when a smaller photographic file matters more than lossless text and line art. WebP support depends on the jsPDF build and browser path you use, so verify it before relying on it.

Export a long page with pagination

A single very tall canvas can exceed browser or device limits and produce a blank, truncated, or partially rendered PDF. The safer pattern is to render the element once, calculate how many source pixels fit on a PDF page, copy each slice into a temporary canvas, and add one PDF page per slice.

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

async function saveLongElementAsPdf(element, filename = 'page.pdf') {
  const canvas = await html2canvas(element, {
    backgroundColor: '#ffffff',
    scale: Math.min(window.devicePixelRatio || 1, 2),
    useCORS: true,
    windowWidth: element.scrollWidth,
    windowHeight: element.scrollHeight,
    logging: false
  });

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

  const margin = 10;
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();
  const targetWidth = pageWidth - margin * 2;
  const targetHeight = pageHeight - margin * 2;

  // Source pixels represented by one PDF page at the chosen width.
  const sourcePageHeight = Math.floor(canvas.width * targetHeight / targetWidth);
  let sourceY = 0;
  let pageNumber = 0;

  while (sourceY < canvas.height) {
    const sliceHeight = Math.min(sourcePageHeight, canvas.height - sourceY);
    const slice = document.createElement('canvas');
    slice.width = canvas.width;
    slice.height = sliceHeight;

    const context = slice.getContext('2d');
    context.fillStyle = '#ffffff';
    context.fillRect(0, 0, slice.width, slice.height);
    context.drawImage(
      canvas,
      0, sourceY, canvas.width, sliceHeight,
      0, 0, slice.width, slice.height
    );

    if (pageNumber > 0) pdf.addPage();
    const displayedHeight = sliceHeight * targetWidth / canvas.width;
    pdf.addImage(slice, 'PNG', margin, margin, targetWidth, displayedHeight);

    sourceY += sliceHeight;
    pageNumber += 1;
  }

  pdf.save(filename);
}

const report = document.querySelector('#report');
document.querySelector('#save-report').addEventListener('click', () => {
  saveLongElementAsPdf(report, 'report.pdf');
});

This preserves the image ratio on every page. A section may be split across two pages because the source is an image. If clean page breaks are required, divide the report into page-sized sections in your markup and capture each section separately.

Avoid awkward breaks

For invoices, dashboards, and reports, add a class to logical blocks and paginate those blocks instead of one giant element. CSS such as break-inside: avoid can help when you use a print-oriented workflow, but html2canvas does not implement a browser print engine. Inspect the actual PDF rather than assuming print CSS will control the canvas.

Useful html2canvas options

Option Purpose Practical note
backgroundColor Sets the canvas background. Use '#ffffff' for predictable paper output or null for transparency.
scale Controls source pixel density. Higher values improve detail and increase memory use.
useCORS Attempts CORS-enabled image loading. It works only when the remote response permits it.
allowTaint Allows drawing resources that taint a canvas. A tainted canvas cannot be exported safely; this does not bypass browser security.
ignoreElements Skips nodes during capture. Use it to remove buttons, navigation, or print-only controls.
onclone Edits the cloned document before rendering. Hide transient UI or change styles without altering the live page.
windowWidth, windowHeight Sets the virtual viewport. For scrollable content, element dimensions can prevent responsive reflow.
scrollX, scrollY Controls the capture scroll position. Useful when capturing a viewport region.

The supported CSS list is documented by the html2canvas project. Unsupported properties can differ from what the user sees in the browser.

Hide controls in the cloned page

const canvas = await html2canvas(document.querySelector('#report'), {
  backgroundColor: '#fff',
  onclone: (clonedDocument) => {
    clonedDocument.querySelectorAll('[data-pdf-ignore]').forEach((node) => {
      node.style.display = 'none';
    });
  }
});

Mark export-only or interactive controls in your HTML:

<button data-pdf-ignore>Delete</button>

Images, fonts, iframes, and browser security

Why are images missing from my PDF? The most common reason is cross-origin content. A browser will not let html2canvas read pixels from an image unless the response supplies compatible CORS headers. Set useCORS: true only when the image host sends those headers. A controlled proxy that fetches permitted assets and returns them with your origin is another option.

allowTaint: true does not make a tainted canvas exportable. With the default setting, html2canvas skips resources that would taint the canvas. Do not use a proxy to retrieve resources your application is not authorized to access.

Same-origin iframes can be rendered recursively. Cross-origin frames and sandboxed frames without allow-same-origin cannot be inspected because of browser security boundaries.

Wait for fonts before capture. jsPDF's standard fonts cover only a limited ASCII codepage. For accented, Cyrillic, Arabic, CJK, or other glyphs, embed a TTF font containing the characters you need and register it with jsPDF. Missing glyphs otherwise appear as blank boxes or garbled text in any text you add directly to the PDF.

When html2pdf.js is a better wrapper

html2pdf.js wraps the same general pipeline: .from() -> .toContainer() -> .toCanvas() -> .toImg() -> .toPdf() -> .save(). It can reduce boilerplate for straightforward exports:

import html2pdf from 'html2pdf.js';

const element = document.querySelector('#report');
html2pdf()
  .set({
    margin: 10,
    filename: 'report.pdf',
    image: { type: 'jpeg', quality: 0.95 },
    html2canvas: { scale: 2, useCORS: true },
    jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
  })
  .from(element)
  .save();

The wrapper's README warns that resizing the root element can cause content reflow. Use the direct API when you need precise slicing, custom page logic, or detailed error handling.

Common errors and fixes

Symptom Likely cause Fix
PDF is blank Canvas exceeded a browser limit, capture ran before layout, or the element had no dimensions. Capture after fonts and images load, check element.getBoundingClientRect(), reduce scale, and paginate smaller sections.
PDF is cut off One-page code used for a taller element. Use the slicing example or capture page-sized sections.
Images are absent Cross-origin response lacks CORS headers. Enable CORS on the asset host, use a permitted proxy, or serve assets from the same origin.
Canvas is tainted A resource was drawn without permission. Fix CORS; allowTaint does not solve export security.
Text or icons look different Unsupported CSS, web fonts not loaded, or a font lacks glyphs. Wait for document.fonts.ready, embed a suitable TTF for jsPDF text, and simplify unsupported styles.
Layout wraps differently The clone used a different viewport width. Set windowWidth deliberately and capture a dedicated export container.
Browser freezes on mobile High-resolution canvas consumes too much memory. Lower scale, remove unnecessary content, and split the export into smaller sections.
PDF contains unsafe content Untrusted markup, text, or resource URLs were passed through. Sanitize input and enforce the application's authorization and URL access rules. The jsPDF README recommends sanitizing user input.

Performance, reliability, and cost considerations

  • Memory: Canvas memory grows with width × height × scale². A scale of 2 can require roughly four times the pixel storage of scale 1.
  • Speed: Hide animations, video, cursors, chat controls, and other unnecessary nodes before capture. Reuse a prepared export container for repeated jobs.
  • Reliability: Test Chromium, Firefox, Safari, and mobile devices you support. Canvas limits depend on browser, platform, and available memory; failures may be blank or partial rather than an explicit exception.
  • Output size: PNG preserves sharp text and line art but can be large. JPEG usually reduces photographic output size at the cost of compression artifacts.
  • Pagination: Smaller captures reduce peak memory and make it easier to retry one failed section.
  • Accessibility: A canvas image in a PDF is not equivalent to selectable, searchable, semantic document text. If accessibility or text extraction is required, generate a structured PDF with a server-side or document-native renderer.
A capture service can remove consent banners and transient widgets before rendering.
A capture service can remove consent banners and transient widgets before rendering.

Or skip the browser setup

When you need a clean website capture rather than a DOM reconstruction inside the user's browser, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you turn each step off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the verdict and billing status with X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options. The simplest call is:

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}`);

You can request full-page captures with lazy images loaded, a CSS-selected element, dark mode, device presets or custom viewports, retina scale, PDF paper sizes and margins, custom CSS and JavaScript, clicks, waits, blocked requests, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs, and usage data. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots; yearly billing provides two months free, and every feature is included on every plan. Create a free ScreenshotNeo account.

FAQ

Does html2canvas make a real screenshot?

No. It reconstructs a canvas from the DOM and supported CSS, so the result can differ from the browser's actual pixels.

Can I export the whole document with document.body?

Yes, but a dedicated content element is usually more predictable. It prevents navigation, controls, overlays, and unrelated page sections from entering the PDF.

Why is my generated PDF cut off?

The canvas or PDF page is too short for the content. Paginate the source canvas or capture smaller sections, then test on target devices.

Can the PDF text be selected?

Not when the page is inserted as one canvas image. Use a structured PDF generator when selectable and semantic text is a requirement.

How do I save a page with Arabic, CJK, or emoji?

Wait for the web font used by the DOM, and embed a TTF with the required glyphs for any text written directly through jsPDF. Verify the font license and glyph coverage.

Should I use html2pdf.js or the two libraries directly?

Use html2pdf.js for a short wrapper workflow. Use html2canvas and jsPDF directly when you need custom pagination, slicing, resource handling, or detailed control.