await snapdom.toPng(element, { dpr: 2 }) returns a loaded HTMLImageElement whose src is a PNG data URL. Use snapdom.download() for a browser download or toBlob({ type: 'png' }) for upload bytes.
What is converted
SnapDOM starts from an element that already exists in a browser. It clones that subtree with the computed styles the browser resolved, inlines images and optional fonts, serializes the clone as HTML inside an SVG foreignObject, then rasterizes that SVG to PNG. The same pipeline handles pseudo-elements, CSS variables, open Shadow DOM, canvas content and same-origin iframes.
Your application still decides when to capture. Wait until data, animations and component rendering have reached the state you want in the file.
Install
npm install @zumer/snapdom@latest
Or load it from a CDN if you don't use a bundler:
<script type="module"> import { snapdom } from "https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.mjs"; </script>
Recipe 1: Capture and show a PNG preview
Wait for document fonts if the element uses them, then capture the rendered node. A fixed dpr makes the pixel output independent of the screen that ran the export.
import { snapdom } from '@zumer/snapdom'; const card = document.querySelector('#card'); if (!card) throw new Error('Missing #card'); await document.fonts.ready; const image = await snapdom.toPng(card, { dpr: 2, embedFonts: true, backgroundColor: '#ffffff', }); document.querySelector('#preview').replaceChildren(image); console.log(image.naturalWidth, image.naturalHeight);
image.naturalWidth and image.naturalHeight are the encoded PNG dimensions. The returned image is already decoded, so it can be inserted immediately.
Recipe 2: Download a PNG file
The download helper rasterizes at DPR 1. Set the file dimensions with width, height or scale, and include the extension in filename.
await snapdom.download(card, { format: 'png', filename: 'product-card.png', width: 1200, embedFonts: true, backgroundColor: '#ffffff', });
With only width set, SnapDOM calculates the height from the capture's aspect ratio. If you prefer to double the element's own dimensions, replace width with scale: 2.
Recipe 3: Preview and upload without capturing twice
Use the two-step API when one DOM clone needs more than one export. The capture and asset inlining run once; toPng() and toBlob() reuse the serialized result.
const capture = await snapdom(card, { dpr: 2, embedFonts: true, }); const preview = await capture.toPng(); document.querySelector('#preview').replaceChildren(preview); const blob = await capture.toBlob({ type: 'png' }); if (!blob) throw new Error('PNG encoding failed'); const form = new FormData(); form.append('file', blob, 'card.png'); const response = await fetch('/api/upload', { method: 'POST', body: form, }); if (!response.ok) { throw new Error(`Upload failed: ${response.status}`); }
toBlob() defaults to SVG. The explicit { type: 'png' } is what makes this Blob contain image/png bytes.
Control the output dimensions
SnapDOM first chooses a logical output size, applies scale, then multiplies the raster by dpr. For a capture with no extra visual bleed:
bitmap width = logical width × scale × dpr bitmap height = logical height × scale × dpr
| Option | Effect | Default |
|---|---|---|
| width / height | Sets the logical output size. One dimension preserves aspect ratio; two dimensions force that exact shape. | element size |
| scale | Multiplies the logical width and height. | 1 |
| dpr | Sets raster density without changing the returned image's CSS display size. | devicePixelRatio |
| backgroundColor | Fills transparent pixels before encoding. | transparent |
| quality | Controls JPEG/WebP encoding. PNG ignores it because PNG is lossless. | 0.92 |
A 600 × 400 element captured with { dpr: 2 } produces a 1200 × 800 bitmap displayed at 600 × 400 CSS pixels. With { scale: 2, dpr: 1 }, the bitmap is also 1200 × 800, but its CSS display size is 1200 × 800.
// Sharp at the same display size const retina = await snapdom.toPng(card, { width: 600, dpr: 2, }); // Exact 1200px-wide file, independent of the device const fixed = await snapdom.toPng(card, { width: 1200, dpr: 1, });
Large values multiply quickly: doubling width and height creates four times as many pixels. Browsers cap a raster at 16,384 pixels per side; SnapDOM downscales an oversized uncropped export and logs a warning. See the high-resolution guide for density choices and the tiled capture guide when one bitmap is too large.
Try the three patterns live
Same element, three buttons: append the PNG, download it, or produce the upload-ready Blob:
Gradients, borders & shadows
Rounded corners, a dashed border, a gradient, captured exactly as rendered.
⚡ Even this shadowPNG, JPEG or WebP
PNG keeps transparency and does not discard image data. The quality option has no effect on it. Use JPEG or WebP when you need a lossy size control; both default to a white background when the source has transparent areas.
const png = await capture.toPng(); const jpeg = await capture.toJpg({ quality: 0.85 }); const webp = await capture.toWebp({ quality: 0.85 });
If an API needs a Base64 data URL instead of a Blob, read png.src. It starts with data:image/png;base64,.
Diagnose a wrong or incomplete capture
| Symptom | Cause and fix |
|---|---|
| External image is missing | The image host did not allow its bytes to be read cross-origin. Serve it from the same origin, add a valid Access-Control-Allow-Origin response, or set useProxy. An HTML crossorigin attribute cannot fix a server that omits CORS headers. |
| Custom font falls back | Wait for document.fonts.ready and pass embedFonts: true. Fonts registered at runtime with FontFace() should also be listed in localFonts. |
| Text wraps differently | Retry with reconcile: true. SnapDOM mounts and measures the styled clone, which fixes rare re-wrap drift but adds another layout pass and can roughly double capture time. |
| Excluded controls leave gaps | exclude uses hidden placeholders by default. Add excludeMode: 'remove' when the remaining content should close the space. |
| The PNG shows stale data | Wait for the application's fetches, framework render and chart animation before calling SnapDOM. The library captures the current browser state; it does not decide when that state is complete. |
| Large capture is downscaled | The requested raster crossed the browser's 16,384-pixel side limit. Lower scale/dpr, set a smaller width/height, or rasterize the reusable capture in cropped bands. |
| Third-party iframe becomes a placeholder | The browser blocks access to cross-origin iframe documents. Same-origin frames are captured; a third-party page must be opened and captured inside a browser you control. |
Set debug: true while diagnosing a capture to expose normally suppressed warnings:
await snapdom.toPng(card, { debug: true });
The complete option contract is in the options reference. For multi-format work, see the reusable capture API.
Frequently asked questions
How do I convert a rendered HTML element to PNG in JavaScript?
Install @zumer/snapdom and call snapdom.toPng(element). The promise resolves to a loaded HTMLImageElement whose src is a PNG data URL.
Why did toBlob return an SVG instead of a PNG?
SVG is the default Blob type. Pass { type: 'png' } to snapdom.toBlob() or result.toBlob() when you need image/png bytes.
How do I control the PNG pixel dimensions?
Set width or height for the logical output size, scale to multiply that size, and dpr for raster density. If you set only one dimension, SnapDOM preserves the aspect ratio.
Does the quality option reduce PNG file size?
No. PNG is lossless and ignores quality. Use toJpg() or toWebp() when you need a quality versus file-size control.
Can SnapDOM convert HTML to PNG in Node.js?
SnapDOM requires a real DOM. In Node.js, open the page with Playwright or Puppeteer, run SnapDOM inside that page, and return the data URL or encoded bytes to Node.
Run a real capture
Use the demo above, then install the same browser API in your application.
Open the demo Install from npm