How to Download HTML as an Image with JavaScript
Render any HTML element to a downloadable PNG with html2canvas, handle CORS and quality, and use ScreenshotNeo for server-side capture.
Short answer: select the element, render it with html2canvas, then download the canvas as a Blob. This handles high-DPI output, CORS-enabled images, and large files without creating a huge data URL.
This recreates the element from its DOM and CSS. It is useful for browser exports, but it is not a native browser screenshot: unsupported CSS, cross-origin content, and cross-origin iframes can differ or fail. For server-side capture without browser setup, see ScreenshotNeo below.
1. Install html2canvas
npm install @html2canvas/html2canvas
html2canvas runs in a browser because it needs window and document. See the html2canvas documentation for browser support.
2. Complete browser example
<article id='capture'>
<h1>Quarterly report</h1>
<p>Revenue grew 18% this quarter.</p>
</article>
<button id='download' type='button'>Download PNG</button>
<script type='module' src='/capture.js'></script>
import html2canvas from '@html2canvas/html2canvas';
document.querySelector('#download').addEventListener('click', downloadElementAsImage);
async function downloadElementAsImage() {
const element = document.querySelector('#capture');
if (!element) throw new Error('Capture element was not found');
if (document.fonts?.ready) await document.fonts.ready;
const canvas = await html2canvas(element, {
scale: window.devicePixelRatio,
useCORS: true,
backgroundColor: '#ffffff'
});
canvas.toBlob((blob) => {
if (!blob) throw new Error('Image encoding failed');
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = 'capture.png';
link.href = url;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
}, 'image/png');
}
See the configuration reference for all options.
3. Formats, quality, scaling, and cropping
canvas.toBlob(done, 'image/jpeg', 0.9); // quality is 0..1
canvas.toBlob(done, 'image/webp', 0.9); // when supported
PNG is lossless and supports transparency. JPEG is smaller for photographs but has no alpha channel. WebP can be requested where supported. Unsupported types fall back to PNG, according to MDN.
Prefer toBlob() with URL.createObjectURL(). MDN’s toDataURL guidance warns that data URLs encode the whole image into an in-memory string.
const rect = element.getBoundingClientRect();
const canvas = await html2canvas(element, {
scale: Math.min(window.devicePixelRatio || 1, 3),
x: 0,
y: 0,
width: rect.width,
height: rect.height,
backgroundColor: null
});
scale sharpens retina output but increases memory and encoding time. The x, y, width, and height options crop the rendered area.
4. Whole-page capture and exclusions
const canvas = await html2canvas(document.body, {
useCORS: true,
scale: window.devicePixelRatio,
ignoreElements: (node) => node.matches?.('[data-html2canvas-ignore]')
});
Mark download buttons or transient controls with data-html2canvas-ignore. Very tall pages can exceed canvas limits, so capture sections separately when needed.
5. Cross-origin assets and fidelity limits
Images must be same-origin or served with CORS headers. useCORS: true cannot bypass browser policy; without Access-Control-Allow-Origin, the canvas may be tainted and export will fail. A proxy can fetch the resource and return it from your origin.
Cross-origin iframes cannot be rendered because their contentDocument is inaccessible. Await document.fonts.ready so web fonts finish loading.
html2canvas reconstructs the DOM and styles rather than taking a native compositor screenshot. Unsupported CSS, filters, blend modes, video, sticky positioning, and animations can differ. Freeze animations and use capture-specific CSS when output must be stable.
6. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| SecurityError or blank image | Cross-origin image tainted the canvas. | Enable CORS, set useCORS: true, or proxy the asset. |
| Iframe missing | Cross-origin iframe DOM is blocked. | Capture inside that origin or use server-side browser capture. |
| Wrong font | Fonts were still loading. | Await document.fonts.ready and check font CORS. |
| Blurry output | Scale is one CSS pixel per output pixel. | Set scale: window.devicePixelRatio, capped for huge images. |
| Crash or huge download | Canvas or data URL used too much memory. | Use toBlob(), lower scale, crop, or split the export. |
| Inconsistent animation | DOM changed during rendering. | Pause animation and capture after data and layout settle. |
| CSS differs | The property is not implemented. | Add capture CSS or use a native browser screenshot service. |
| No download | Missing user gesture or null Blob. | Call from a click handler and check the Blob result. |
7. Performance, reliability, and cost
- Render only the required element and avoid repeated captures during interaction.
- Use the lowest acceptable scale and release object URLs.
- Wait for fonts and data, then capture after layout stabilizes.
- Browser capture has no API charge but uses the user’s CPU, memory, network, and canvas limits.
- For untrusted pages, browser policy is a security boundary; server-side capture is more suitable for consistent access to remote content.
8. Or skip the browser setup
ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one HTTP request. It supports full-page or CSS-element capture, custom CSS and JavaScript, waits, device presets, headers, cookies, blocking rules, caching, async jobs, and bulk capture. Read the ScreenshotNeo API docs.
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
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)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
const fs = require('node:fs/promises');
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
The Free plan includes 1,000 shots each 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.
9. FAQ
Can I download a div without a library?
You can draw simple known content yourself, but arbitrary HTML and CSS need a renderer such as html2canvas or a browser screenshot service.
Which format should I choose?
Use PNG for text, diagrams, and transparency; JPEG for photographs; WebP when your consumers support it.
Why does toDataURL work in a demo but fail in production?
Production pages are larger and often include cross-origin assets. Memory use grows with the encoded string, and canvas security rules still apply.
Can this run in Node.js?
html2canvas needs a browser DOM. In Node, use browser automation or an HTTP screenshot API such as ScreenshotNeo.


