Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →The reliable fix is to treat export as three separate stages: capture the mounted React element, verify that html-to-image produced image data, then place that image in a correctly sized jsPDF document. Most blank, clipped, or missing-image PDFs fail in one of those stages—not in the download button itself.
This guide gives a complete client-side implementation, then shows how to diagnose cross-origin assets, CSS differences, canvas limits, pagination, and the trade-off between image PDFs and selectable text.
Install the libraries and prepare the component
Install the two packages in the application that renders the content:
npm install html-to-image jspdf
Use a ref on the exact mounted node that should appear in the file. Do not capture a component before its data, fonts, and images have finished loading.
#1 Best Overall
import { useRef, useState } from 'react';
import { toPng } from 'html-to-image';
import { jsPDF } from 'jspdf';
export default function Invoice() {
const invoiceRef = useRef(null);
const [exporting, setExporting] = useState(false);
async function exportPdf() {
const node = invoiceRef.current;
if (!node) return;
setExporting(true);
try {
// Wait for images that are already in the DOM.
await Promise.all(
Array.from(node.querySelectorAll('img')).map((img) =>
img.complete
? Promise.resolve()
: new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
})
)
);
const dataUrl = await toPng(node, {
cacheBust: true,
pixelRatio: 2,
backgroundColor: '#ffffff'
});
const image = new Image();
image.src = dataUrl;
await image.decode();
const pdf = new jsPDF({
orientation: image.width > image.height ? 'landscape' : 'portrait',
unit: 'px',
format: [image.width, image.height]
});
pdf.addImage(dataUrl, 'PNG', 0, 0, image.width, image.height);
pdf.save('invoice.pdf');
} catch (error) {
console.error('PDF export failed:', error);
// Show an application error message here.
} finally {
setExporting(false);
}
}
return (
<>
<button type="button" onClick={exportPdf} disabled={exporting}>
{exporting ? 'Preparing PDF…' : 'Download PDF'}
</button>
<section ref={invoiceRef} className="invoice">
{/* The content to export */}
</section>
</>
);
}
html-to-image exposes promise-based methods including toPng, toJpeg, toSvg, toBlob, toCanvas, and toPixelData. jsPDF’s addImage API accepts data URLs, image elements, and canvas elements. Keeping those calls separate makes it clear whether the capture or the PDF insertion is failing.
Choose image and page dimensions deliberately
The example uses CSS pixels as the PDF unit and creates a page exactly as large as the captured image. That avoids an accidental crop, but it produces a custom-sized page. For a standard A4 page, calculate a scale that fits the image instead:
const pdf = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const ratio = Math.min(pageWidth / image.width, pageHeight / image.height);
const width = image.width * ratio;
const height = image.height * ratio;
const x = (pageWidth - width) / 2;
const y = (pageHeight - height) / 2;
pdf.addImage(dataUrl, 'PNG', x, y, width, height);
pdf.save('invoice-a4.pdf');
Use JPEG when a photographic page matters more than lossless text edges, and PNG when crisp UI text or transparency is important. A larger pixelRatio improves detail but increases memory use and the PDF’s image payload.
Exporting long content without clipping
A single very tall canvas can exceed a browser’s maximum canvas dimensions. The html2canvas FAQ identifies canvas limits as a cause of empty or cut-off output. Although html-to-image uses a different rendering path, the same browser memory and bitmap limits apply to its canvas conversion.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsRank #2
For reports longer than one page, capture intentional sections rather than one enormous node:
- Render each page-sized section with a stable width and a white background.
- Call
toPng(section)for one section at a time. - Add the first image to the PDF, call
pdf.addPage(), and add each subsequent image at the same coordinates. - Release references to large data URLs when the export finishes.
const sections = Array.from(document.querySelectorAll('.pdf-page'));
const pdf = new jsPDF({ unit: 'mm', format: 'a4' });
for (let i = 0; i < sections.length; i += 1) {
const dataUrl = await toPng(sections[i], { pixelRatio: 2, backgroundColor: '#fff' });
if (i > 0) pdf.addPage();
pdf.addImage(dataUrl, 'PNG', 0, 0, 210, 297);
}
pdf.save('report.pdf');
Do not assume a CSS page break will be honored by an image capture; page boundaries must be represented by the nodes you capture or by your own slicing logic.
Why images and fonts disappear
Cross-origin images
Browser security prevents a canvas from reading pixels from an image that the page is not allowed to access. The html2canvas FAQ recommends either a remote server response containing an appropriate Access-Control-Allow-Origin header or a same-origin proxy. Its useCORS option defaults to false, and enabling it cannot make an uncooperative image host grant permission.
Check the browser network panel for failed image, font, stylesheet, and CSS background-image requests. Confirm the deployed origin, response headers, redirects, and authentication. If you control the asset server, serve the files with CORS headers and use URLs that do not require a cookie unavailable to the capture request. Otherwise proxy the asset through your own origin, subject to the asset owner’s terms.
Fonts and webfont timing
Capture only after the intended font is loaded. A practical guard is await document.fonts.ready immediately before toPng. A font request that fails or is blocked can make text wrap differently even when the rest of the page works.
Data URLs and diagnostics
Inspect the value returned by html-to-image. It should begin with a data URL such as data:image/png;base64,. If the promise rejects, log the complete error and fix the resource or rendering problem before calling jsPDF. If a valid data URL exists but the PDF is blank, test a plain local image and verify the format, coordinates, and non-zero width and height passed to addImage.
Why the PDF does not match the browser
DOM-to-image tools reconstruct a supported subset of the page; they do not guarantee a pixel-identical browser screenshot. The html2canvas documentation explains that its renderer uses DOM information and only understands certain CSS. html-to-image uses SVG foreignObject and canvas; its README documents browser and security limitations, including stricter Safari handling of foreignObject and a Firefox issue involving some external stylesheets.
- Start with a small test node and add complex styles gradually.
- Replace unsupported effects, filters, or dynamically generated backgrounds with simpler equivalents for the export view.
- Give the export node explicit width, height, colors, and overflow rules instead of relying on viewport-dependent layout.
- Compare the generated PNG before passing it to jsPDF; this identifies whether the difference is in rendering or PDF placement.
Blank, clipped, or huge files: a decision path
The capture promise rejects
Look at the first console or network error. It is commonly a cross-origin image/font, inaccessible stylesheet, unsupported resource, or a node that has been unmounted. Verify ref.current, wait for assets, and retry with a minimal node.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #4
The PNG is blank or cut off
Measure node.scrollWidth and node.scrollHeight; make sure the element is not hidden with display: none, has non-zero dimensions, and is not clipped by an ancestor. Reduce pixelRatio, capture sections, and avoid a single canvas larger than the browser permits. For html2canvas-based code, its configuration reference documents explicit width, height, scale, and viewport controls.
The PDF downloads but content is missing
Confirm the image type passed to addImage matches the data (PNG for a PNG data URL, JPEG for JPEG), and check that x, y, width, and height are inside the page. Use a known local test image to separate jsPDF setup from DOM rendering.
The file is too large or text cannot be selected
A rasterized PDF stores the page as an image. The html2pdf.js README documents the resulting loss of selectable/searchable text and potentially large files. If search, accessibility, copy/paste, or crisp zoom is a requirement, generate a PDF whose text and graphics are PDF objects instead of rasterizing the entire DOM. That is a different architecture, not a setting that can be switched on in addImage.
When jsPDF’s HTML method is appropriate
jsPDF also offers an html method. Its official documentation identifies html2canvas as an optional dependency and DOMPurify when the input is an HTML string; bundlers may load those dependencies as separate chunks. This can reduce your own capture code, but it still inherits html2canvas’s browser, CSS, CORS, and canvas constraints. It is not a fix for an image host that denies CORS.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Best Value
Make exports reliable in production
- Disable the export button while a capture is running to prevent overlapping large canvases.
- Show a useful error and keep the original exception in logs; a click event alone does not prove that capture succeeded.
- Capture after React state settles. If a button changes the export view, await a render boundary or trigger export from a subsequent user action.
- Use an export-only stylesheet with stable dimensions and a white background.
- Test the browsers you support, especially Safari and Firefox when using external stylesheets or SVG
foreignObject. - Keep sensitive content in mind: a client-side PDF contains whatever the user can render and save.
Or skip the browser setup
If your requirement is a clean screenshot or PDF of a URL rather than a selectable-text document assembled from React data, ScreenshotNeo handles the browser session for you. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
One request returns PNG, JPEG, WebP, or PDF:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for options such as full-page capture with lazy images, CSS-selector elements, dark mode, device presets, retina scale, PDF paper and page ranges, custom CSS or JavaScript, click and wait conditions, blocked resources, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous webhooks, bulk capture, usage, and the OpenAPI specification.
Equivalent calls from common runtimes:
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}`);
The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account to try the URL-based workflow.
FAQ
Can I keep the PDF’s text selectable while using html-to-image?
No. html-to-image produces a bitmap or SVG representation for insertion. Selectable text requires a PDF-generation path that places text as PDF content rather than one raster image.
Recommended Free Tools
Should I use PNG or JPEG for a dashboard?
PNG is the safer default for UI text, charts, and transparency. JPEG can reduce size for photographic content but introduces lossy compression.
Why does a local image work but the production image fail?
Production often changes the origin, redirects, authentication, or CORS response headers. Compare the deployed request and headers in the network panel; the browser’s origin policy still applies in production.
Frequently Asked Questions
Can a hidden React node be exported?
It must have renderable dimensions when captured. A node with display:none has no layout; render an export-only node off-screen or temporarily make it visible instead.
Does increasing pixelRatio always improve quality?
It increases bitmap resolution until memory or canvas limits become the bottleneck. Increase it gradually and reduce it or split pages when captures become blank, clipped, or slow.
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.

