HTML2Canvas Basics
Learn how html2canvas turns DOM elements into images, handle CORS and CSS limits, and choose a server-side screenshot option when needed.
html2canvas turns a DOM element into a <canvas> asynchronously. It reads the element’s DOM and computed styles, then reconstructs an image from the CSS features it supports. It does not capture the browser’s already-rendered pixels, so the result can differ from the live page.
The smallest working example is:
import html2canvas from 'html2canvas';
const element = document.querySelector('#capture');
const canvas = await html2canvas(element);
document.body.appendChild(canvas);
Install it with npm, yarn, or pnpm as documented in the official getting-started guide.
What html2canvas actually does
html2canvas walks the DOM, reads styles, and paints a representation onto a canvas. The project documentation describes this as a reconstruction rather than an actual screenshot: the output “may not be 100% accurate to the real representation” because it is built from information available on the page. See the official explanation.
This distinction determines when to use it:
- Use it for client-side exports of a component, chart, invoice, card, or page section.
- Expect differences for CSS that html2canvas does not implement.
- Use a browser screenshot API or headless browser when you need the browser’s rendered pixels, server-side execution, or pages outside the current document.
Install and create a complete browser example
1. Create the page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>html2canvas example</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
#capture { width: 640px; padding: 2rem; background: #f4f7fb; color: #172033; }
.badge { display: inline-block; padding: .35rem .6rem; border-radius: 999px; background: #dbeafe; }
</style>
</head>
<body>
<section id="capture">
<span class="badge">Exportable card</span>
<h1>A DOM element becomes a canvas</h1>
<p>This section is the element html2canvas will reconstruct.</p>
</section>
<button id="save">Save PNG</button>
<script type="module" src="./app.js"></script>
</body>
</html>
2. Install the package
npm install html2canvas
3. Render and download the canvas
import html2canvas from 'html2canvas';
const button = document.querySelector('#save');
button.addEventListener('click', async () => {
const element = document.querySelector('#capture');
const canvas = await html2canvas(element);
const link = document.createElement('a');
link.download = 'capture.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
The Promise must resolve before you append the canvas, call toDataURL(), or convert it to a blob.
Useful options and how to choose them
| Option | When to use it | Important limitation |
|---|---|---|
useCORS: true |
Remote images are served with an appropriate CORS response header. | It cannot bypass browser security policy. |
proxy |
A server you control fetches remote resources and returns them for the page. | The proxy must be configured correctly and must respect the remote server’s policy. |
windowWidth, windowHeight |
Long or clipped captures need dimensions matching the element’s scroll area. | Browser and device canvas limits vary. |
scale |
Adjust output density for exports. | A larger scale also increases memory use and canvas dimensions. |
Pass options as the second argument:
const canvas = await html2canvas(document.querySelector('#capture'), {
useCORS: true,
windowWidth: document.documentElement.scrollWidth,
windowHeight: document.documentElement.scrollHeight
});
Check the project’s configuration reference for the current option names and defaults. Keep your first reproduction small: capture one element, inspect the result, then add options one at a time.
Cross-origin images and tainted canvases
If an image comes from another origin, the browser can taint the canvas. A tainted canvas cannot be read with toDataURL() or toBlob(). The official FAQ recommends useCORS: true only when the image host sends a suitable Access-Control-Allow-Origin header, or a properly configured proxy. Neither approach evades browser security rules.
const canvas = await html2canvas(element, {
useCORS: true
});
canvas.toBlob(blob => {
if (!blob) throw new Error('Canvas could not be read');
// Upload or download the blob here.
}, 'image/png');
When debugging, open the browser’s Network panel and inspect the image response headers. A failed CORS preflight, a redirect to a host without CORS headers, or an image loaded from a different origin can explain missing pixels.
Why the output differs from the page
html2canvas implements CSS properties individually. The project states that it will never have full CSS support because every property must be implemented manually. Differences commonly appear with unsupported or partially supported layout, visual, and filter features.
- Reduce the page to a minimal element and compare it with the live page.
- Replace the suspect property with a simpler equivalent.
- Check the current supported-features list.
- Capture after fonts, images, and dynamic content have finished loading.
For pixel-level fidelity, use a browser-controlled screenshot instead of a DOM reconstruction.
Long pages, large elements, and blank output
Canvas dimensions have browser and device limits. Very large captures can be blank, truncated, or partially rendered. The FAQ suggests matching windowWidth and windowHeight to the element’s scroll dimensions when needed, while warning that safe limits vary by platform.
const element = document.querySelector('#capture');
const canvas = await html2canvas(element, {
windowWidth: element.scrollWidth,
windowHeight: element.scrollHeight
});
For a very long document, capture smaller sections and combine them in a format designed for paging, or use a server-side browser PDF workflow. Increasing scale multiplies memory pressure, so start at the default and raise it only when the output is too soft.
Timing dynamic content
html2canvas reads the DOM at capture time. Wait until application data, web fonts, images, and animations are in the state you want to export.
await document.fonts.ready;
await Promise.all(
[...document.images].map(image => image.complete
? Promise.resolve()
: new Promise(resolve => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
}))
);
const canvas = await html2canvas(document.querySelector('#capture'));
Pause animations or add a stable export class if a moving element produces inconsistent frames.
Can html2canvas run in Node.js?
No. It depends on browser objects such as window, document, and computed styles. The official documentation describes it as browser software. For server-side screenshots, its FAQ names Puppeteer and Playwright, which drive a headless browser. For browser extensions, use the browser’s native extension screenshot APIs.
cURL, Python, and Node.js alternatives
html2canvas itself is a browser library, so cURL cannot invoke it. If your application needs a URL screenshot from a server, use a screenshot API or run a headless browser.
Or skip the browser setup
ScreenshotNeo accepts one GET request and returns a PNG, JPEG, WebP, or PDF. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
Read the ScreenshotNeo API documentation for all options.
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 same features. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| Images are missing | Cross-origin response lacks CORS headers. | Use useCORS: true only with suitable headers, or configure a proxy. |
toDataURL() throws a security error |
The canvas is tainted by remote image data. | Fix CORS or proxy the resource; browser policy cannot be bypassed. |
| Fonts or content are stale | Capture ran before resources or application data finished loading. | Await application readiness, document.fonts.ready, and image completion. |
| Layout differs from the page | A CSS property is unsupported or partially implemented. | Check supported features and simplify the reproduction. |
| Long capture is blank or cut off | Canvas exceeded a browser or device limit. | Reduce the region, lower scale, set scroll dimensions, or use a headless browser. |
| Nothing renders in Node.js | There is no browser DOM. | Run html2canvas in a browser, or use Puppeteer/Playwright server-side. |
Performance, reliability, and cost considerations
- Performance: Capture only the required element, avoid oversized scale values, and wait for resources once rather than repeatedly rendering.
- Reliability: Keep a minimal test page for each target browser. Browser limits and CSS support vary, so validate the exact layouts you ship.
- Security: Treat remote images and proxy endpoints as part of your browser security model. Do not assume a proxy makes untrusted content safe.
- Cost: html2canvas runs in the user’s browser and has no API request cost, but it consumes client CPU and memory. A hosted API adds request usage; ScreenshotNeo bills only clean shots and provides billing headers for each response.
FAQ
Does html2canvas take a real screenshot?
No. It reconstructs an image from DOM and styles.
Does it capture content outside the selected element?
The call starts from the element you pass. Select the document root when you need a larger region, subject to canvas limits.
Can I use it for a browser extension?
The official FAQ recommends native extension screenshot APIs for extension capture.
What should I use for server-side URL screenshots?
Use Puppeteer or Playwright, or a hosted screenshot API such as ScreenshotNeo.
Where can I verify current support?
Use the project’s supported-features page, FAQ, and examples.


