Render the page once with html2canvas, scale that canvas to your PDF’s printable width, then place the same image on successive jsPDF pages with a progressively negative y offset. That produces a reliable, screenshot-style multi-page PDF in the browser. Use section-by-section rendering instead when headings, cards, or table rows must never be split.
The three layers: render, scale, paginate
1. Render the DOM
html2canvas(element, options) walks the DOM and builds a canvas representation from the properties it understands. It is not a raw browser screenshot: unsupported CSS, unloaded fonts, blocked images, filters, and complex effects can differ from the live page. The html2canvas project describes the result as a screenshot of a webpage or part of it created in the user’s browser.
2. Scale to the PDF page
Choose a PDF format and unit first. In the example below, jsPDF uses A4 millimetres. Subtract margins from the page width, then scale the canvas proportionally so its full width fits. Its resulting height is the height of the complete web page in PDF units.
3. Paginate the scaled image
jsPDF’s addImage accepts a canvas or image data and explicit x, y, width, and height coordinates. Put the image at the margin on page one. For page two and later, add a page and move the same image upward by one printable page height. The visible page window then shows the next vertical slice.
#1 Best Overall
Prepare the HTML for capture
Give the export area a stable selector and make its visual state deterministic. Wait for asynchronous content before capturing, set a background color, and ensure the element has the full height you intend to export.
<main id="print-area">
<h1>Quarterly report</h1>
<section class="card">...content...</section>
</main>
<button id="download-pdf">Download PDF</button>
Images should be same-origin or served with appropriate CORS headers. A cross-origin iframe cannot be read through its contentDocument, so its contents cannot be rendered by html2canvas in the parent page.
Continuous multi-page export with jsPDF
Install the packages with your package manager, import them, and attach the function to a button. This complete pattern calculates page count from the scaled PDF height, not from raw canvas pixels.
import { jsPDF } from 'jspdf';
import html2canvas from 'html2canvas';
async function downloadPdf() {
const element = document.querySelector('#print-area');
if (!element) throw new Error('Cannot find #print-area');
// Capture after your application has loaded fonts, images, and data.
const canvas = await html2canvas(element, {
scale: Math.min(window.devicePixelRatio || 1, 2),
useCORS: true,
backgroundColor: '#ffffff'
});
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
const margin = 10;
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const printableWidth = pageWidth - margin * 2;
const scaledHeight = canvas.height * printableWidth / canvas.width;
const printableHeight = pageHeight - margin * 2;
const pageCount = Math.ceil(scaledHeight / printableHeight);
const image = canvas.toDataURL('image/jpeg', 0.92);
for (let page = 0; page < pageCount; page += 1) {
if (page > 0) pdf.addPage();
const y = margin - page * printableHeight;
pdf.addImage(image, 'JPEG', margin, y, printableWidth, scaledHeight);
}
pdf.save('website.pdf');
}
document.querySelector('#download-pdf').addEventListener('click', downloadPdf);
Why the negative offset works
Suppose the scaled image is 500 mm tall and the printable region is 277 mm. Page one draws it at y = 10, showing the top 277 mm. Page two draws it at y = 10 - 277, aligning the next slice with the page’s top margin. The loop repeats until the complete image is visible.
JPEG, PNG, and quality
The example uses JPEG at quality 0.92, which generally keeps a photographic or screenshot-style document compact. Use canvas.toDataURL('image/png') and 'PNG' when sharp text, transparency, or flat graphics matter more than file size. A larger html2canvas scale produces more pixels but consumes more memory; cap it on very long pages.
Do not place important text exactly on a page boundary. Anti-aliasing can create a faint seam where two slices meet. A small overlap can hide that seam, but it may duplicate a thin strip. For documents where that trade-off is unacceptable, use page-aware sections.
Prevent cards and headings from being cut
Render one planned section per PDF page
Wrap each page-sized block in a class such as .pdf-page, render each block separately, and add one image to each jsPDF page. This gives you exact boundaries and makes repeating headers straightforward, but your HTML must already be page-aware.
async function addSection(pdf, section, margin = 10) {
const canvas = await html2canvas(section, {
scale: Math.min(window.devicePixelRatio || 1, 2),
useCORS: true,
backgroundColor: '#fff'
});
const width = pdf.internal.pageSize.getWidth() - margin * 2;
const height = canvas.height * width / canvas.width;
pdf.addImage(canvas, 'PNG', margin, margin, width, height);
}
async function downloadPlannedPdf() {
const sections = [...document.querySelectorAll('.pdf-page')];
const pdf = new jsPDF({ unit: 'mm', format: 'a4' });
for (let i = 0; i < sections.length; i += 1) {
if (i > 0) pdf.addPage();
await addSection(pdf, sections[i]);
}
pdf.save('planned.pdf');
}
Measure each section against the printable height. If a section is taller than one page, split it deliberately rather than allowing a browser bitmap to decide where it breaks.
Use html2pdf.js for CSS page-break rules
html2pdf.js is a client-side wrapper around html2canvas and jsPDF. Its page-break configuration supports CSS rules, legacy markers, and explicit before, after, and avoid selectors.
html2pdf()
.set({
margin: 10,
filename: 'website.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: {
mode: ['css', 'legacy'],
before: '.start-new-page',
avoid: ['img', '.card']
}
})
.from(document.querySelector('#print-area'))
.save();
The wrapper’s image setting supports JPEG, PNG, and WebP where the canvas implementation supports them; its quality value applies to JPEG and WebP. Page-break avoidance is a request, not a guarantee: an element taller than the available page cannot remain intact without being split or redesigned.
Choosing an approach
| Approach | Page-boundary control | Implementation effort | Visual fidelity | Text/searchability | Headers and footers |
|---|---|---|---|---|---|
| Direct jsPDF offset loop | Low; continuous slices | Lowest | Screenshot-like, limited by html2canvas | Bitmap; text is not selectable | Draw after every addPage() |
| Section-by-section jsPDF | High when HTML is page-aware | Medium | Good for planned sections | Bitmap; text is not selectable | Easy to include per section |
| html2pdf.js | Medium to high through selectors and CSS | Low | Same html2canvas limitations | Bitmap output | Use page-sized sections or application code |
| PDF layout/server renderer | Highest for semantic flow | Highest | Depends on the renderer | Can preserve real text | Supports document-level layout features |
Choose the offset loop for a faithful continuous capture, section rendering when a card or heading must stay together, and a PDF layout engine when selectable text, accessibility, repeating table headers, or precise widow/orphan control is a requirement. Those capabilities are outside html2canvas and jsPDF’s bitmap workflow.
Headers, footers, and page numbers
addImage only places the captured image; repetition is your code. In the continuous loop, draw a header or footer after each image placement so it remains above the bitmap:
Free tools Windows power users keep installed
One-click scans. No signup required.
for (let page = 0; page < pageCount; page += 1) {
if (page > 0) pdf.addPage();
pdf.addImage(image, 'JPEG', margin, margin - page * printableHeight,
printableWidth, scaledHeight);
pdf.setFontSize(9);
pdf.text(`Page ${page + 1} of ${pageCount}`, pageWidth - margin, pageHeight - 5, {
align: 'right'
});
}
A captured header inside the original canvas will scroll with the page image. To repeat it independently, reserve space in your PDF layout or render page-sized sections containing that header.
Troubleshooting common failures
Blank or missing images
- Cause: a cross-origin image taints the canvas or lacks permission for browser reuse.
- Fix: host the asset on the same origin, configure the asset server’s CORS headers, wait for the image to finish loading, and keep
useCORS: true. Do not expectuseCORSto override a server that sends no permission.
Cross-origin iframe content is absent
- Cause: browser same-origin policy blocks access to the iframe’s document.
- Fix: render content you control in the parent page, proxy it through a permitted origin, or generate the PDF in a renderer that can load the source separately. The parent-page html2canvas call cannot read an unrelated origin’s iframe.
CSS does not match the live page
- Cause: html2canvas reproduces only CSS properties it understands, or capture happened before fonts, images, or dynamic data were ready.
- Fix: simplify unsupported effects, set an explicit background, await
document.fonts.readywhere available, wait for images, and capture after layout has settled.
The PDF has unexpected extra pages
- Cause: page count was calculated from raw pixels, or the canvas is taller than expected.
- Fix: calculate
scaledHeightafter width scaling, then useMath.ceil(scaledHeight / printableHeight). Inspectcanvas.width,canvas.height, and the element’s computed dimensions.
Headings or cards split across pages
- Cause: the offset loop knows nothing about semantic boundaries.
- Fix: render planned sections separately, or configure html2pdf.js with
avoidand explicitbeforeselectors. Redesign elements taller than one page.
Memory errors on very long pages
- Cause: a high-resolution canvas stores every pixel of the entire document.
- Fix: lower
scale, capture sections independently, reduce unnecessary page width, and release references after saving. There is no universal maximum canvas size; browser and device limits vary.
Performance, reliability, and output limits
Capture only the required element instead of the entire document. Keep the scale no higher than the resolution your PDF needs, and avoid duplicating a huge data URL in application state. Wait for network-loaded images and fonts before starting. A continuous canvas is simplest but makes one very tall bitmap; section rendering reduces peak size and gives recovery points if one section fails.
Client-side generation runs in the visitor’s browser, so results vary with available memory, browser security policy, fonts, and asset servers. There are no dependable speed, file-size, or browser-percentage figures to promise for every page. Test representative pages, especially those containing large images, charts, sticky elements, web fonts, or third-party embeds.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo returns a website screenshot or PDF from one GET request, so your server or script does not need to configure a browser. Its capture pipeline accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each cleanup step can be disabled. Bot checks and CAPTCHAs, 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.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
For a direct PDF or image workflow, see the ScreenshotNeo documentation. The same endpoint supports full-page capture with lazy images, CSS-selector element capture, device and viewport settings, retina scale, custom CSS and JavaScript, clicks, waits, blocked requests, cookies, headers, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous webhooks, bulk capture, usage data, and PDF paper, margin, orientation, and page-range options. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
Rank #4
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}`);
Every plan includes the features above. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Yearly billing gives two months free. Create a free ScreenshotNeo account to make your first captures.
Frequently Asked Questions
Can jsPDF create selectable text from html2canvas?
No. This workflow places a raster canvas in the PDF, so text behaves like pixels. Use a PDF layout engine or server-side renderer when selectable text or accessibility is required.
Why is my second page blank?
Check that the image is placed with a negative offset based on the printable page height, that page count uses the scaled height, and that the source canvas is not blank because of blocked assets.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Should I use PNG or JPEG?
Use JPEG for smaller screenshot-style files and PNG for sharp flat graphics or transparency. The choice does not change html2canvas’s CSS or cross-origin limitations.
Quick Recap
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.

