snapDOM is a fast and accurate DOM-to-image capture tool built for Zumly, a zoom-based view transition framework.
It captures any HTML element as a scalable SVG image, preserving styles, fonts, background images, pseudo-elements, and even shadow DOM. It also supports export to raster image formats and canvas.
- 📸 Full DOM capture
- 🎨 Embedded styles, pseudo-elements, and fonts
- 🖼️ Export to SVG, PNG, JPG, WebP,
canvas
, or Blob - ⚡ Ultra fast, no dependencies
- 📦 100% based on standard Web APIs
https://zumerlab.github.io/snapdom/
- Installation
- Basic usage
- API
- Options
- Limitations
- ⚡ Performance Benchmarks (Chromium)
- Roadmap
- Development
- Contributors 🙌
- 💖 Sponsors
- Star History
- License
npm i @zumer/snapdom
yarn add @zumer/snapdom
Then import it in your code:
import { snapdom } from '@zumer/snapdom';
<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.min.js"></script>
<script src="snapdom.js"></script>
import { snapdom } from './snapdom.mjs';
<script type="module">
import { snapdom } from 'https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.mjs';
</script>
const el = document.querySelector('#target');
const result = await snapdom(el);
const img = await result.toPng();
document.body.appendChild(img);
await result.download({ format: 'jpg', filename: 'my-capture' });
const el = document.querySelector('#target');
const png = await snapdom.toPng(el);
document.body.appendChild(png);
const blob = await snapdom.toBlob(el);
Returns an object with reusable export methods:
{
url: string;
toRaw(): string;
toImg(): Promise<HTMLImageElement>;
toCanvas(): Promise<HTMLCanvasElement>;
toBlob(options?): Promise<Blob>;
toPng(options?): Promise<HTMLImageElement>;
toJpg(options?): Promise<HTMLImageElement>;
toWebp(options?): Promise<HTMLImageElement>;
download(options?): Promise<void>;
}
Method | Description |
---|---|
snapdom.toImg(el, options?) |
Returns an HTMLImageElement |
snapdom.toCanvas(el, options?) |
Returns a Canvas |
snapdom.toBlob(el, options?) |
Returns an SVG or raster Blob |
snapdom.toPng(el, options?) |
Returns a PNG image |
snapdom.toJpg(el, options?) |
Returns a JPG image |
snapdom.toWebp(el, options?) |
Returns a WebP image |
snapdom.download(el, options?) |
Triggers a download |
✅ Note: Style compression is now always on internally. The
compress
option has been removed.
All capture methods accept an options
object:
Option | Type | Default | Description |
---|---|---|---|
fast |
boolean | true |
Skips small idle delays for faster results |
embedFonts |
boolean | false |
Inlines non-icon fonts (icon fonts always on) |
localFonts |
array | [] |
Local fonts { family, src, weight?, style? } |
iconFonts |
string|RegExp|Array | [] |
Extra icon font matchers |
excludeFonts |
object | {} |
Exclude font families/domains/subsets during embedding |
scale |
number | 1 |
Output scale multiplier |
dpr |
number | devicePixelRatio |
Device pixel ratio |
width |
number | - | Output width |
height |
number | - | Output height |
backgroundColor |
string | "#fff" |
Fallback color for JPG/WebP |
quality |
number | 1 |
Quality for JPG/WebP (0 to 1) |
useProxy |
string | '' |
Proxy base for CORS fallbacks |
type |
string | svg |
Default Blob type (svg |png |jpg |webp ) |
exclude |
string[] | - | CSS selectors to exclude |
filter |
function | - | Custom predicate (el) => boolean |
cache |
string | "soft" |
Control internal caches: disabled , soft , auto , full |
defaultImageUrl |
string | function | - | Fallback image when an <img> fails. If a function is provided, it receives { width?, height?, src?, element } and must return a URL (string or Promise). Useful for placeholder services (e.g. https://placehold.co/{width}x{height} ) |
Provide a default image for failed <img>
loads. You can pass a fixed URL or a callback that receives measured dimensions and returns a URL (handy to generate dynamic placeholders).
// 1) Fixed URL fallback
await snapdom.toImg(element, {
defaultImageUrl: '/images/fallback.png'
});
// 2) Dynamic placeholder via callback
await snapdom.toImg(element, {
defaultImageUrl: ({ width = 300, height = 150 }) =>
`https://placehold.co/${Math.round(width)}x${Math.round(height)}`
});
// 3) With proxy (if your fallback host has no CORS)
await snapdom.toImg(element, {
defaultImageUrl: ({ width = 300, height = 150 }) =>
`https://dummyimage.com/${Math.round(width)}x${Math.round(height)}/cccccc/666.png&text=img`,
useProxy: 'https://corsproxy.io/?url='
});
Notes:
- If the fallback image also fails to load, snapDOM replaces the
<img>
with a placeholder block preserving width/height. - Width/height used by the callback are gathered from the original element (dataset, style/attrs, etc.) when available.
- If
scale
is provided, it takes precedence overwidth
/height
. - If only
width
is provided, height scales proportionally (and vice versa). - Providing both
width
andheight
forces an exact size (may distort).
By default snapDOM tries crossOrigin="anonymous"
(or use-credentials
for same-origin). If an asset is CORS-blocked, you can set useProxy
to a prefix URL that forwards the actual src
:
await snapdom.toPng(el, {
useProxy: 'your-proxy' // Example 'https://api.allorigins.win/raw?url='
});
Tips
- Keep the proxy fast and cache-friendly (adds big wins on repeated captures).
- The proxy is only used as a fallback; same-origin and CORS-enabled assets skip it.
When true
, snapDOM embeds non-icon @font-face
rules detected as used within the captured subtree. Icon fonts (Font Awesome, Material Icons, etc.) are embedded always.
If you serve fonts yourself or have data URLs, you can declare them here to avoid extra CSS discovery:
await snapdom.toPng(el, {
embedFonts: true,
localFonts: [
{ family: 'Inter', src: '/fonts/Inter-Variable.woff2', weight: 400, style: 'normal' },
{ family: 'Inter', src: '/fonts/Inter-Italic.woff2', style: 'italic' }
]
});
Add custom icon families (names or regex matchers). Useful for private icon sets:
await snapdom.toPng(el, {
iconFonts: ['MyIcons', /^(Remix|Feather) Icons?$/i]
});
Skip specific non-icon fonts to speed up capture or avoid unnecessary downloads.
await snapdom.toPng(el, {
embedFonts: true,
excludeFonts: {
families: ['Noto Serif', 'SomeHeavyFont'], // skip by family name
domains: ['fonts.gstatic.com', 'cdn.example'], // skip by source host
subsets: ['cyrillic-ext'] // skip by unicode-range subset tag
}
});
Notes
excludeFonts
only applies to non-icon fonts. Icon fonts are always embedded.- Matching is case-insensitive for
families
. Hosts are matched by substring against the resolved URL.
exclude
: remove by selector.filter
: advanced predicate per element (returnfalse
to drop).
Example: filter out elements with display:none
:
/**
* Example filter: skip elements with display:none
* @param {Element} el
* @returns {boolean} true = keep, false = exclude
*/
function filterHidden(el) {
const cs = window.getComputedStyle(el);
if (cs.display === 'none') return false;
return true;
}
await snapdom.toPng(document.body, { filter: filterHidden });
Example with exclude
: remove banners or tooltips by selector
await snapdom.toPng(el, {
exclude: ['.cookie-banner', '.tooltip', '[data-test="debug"]']
});
Preloads external resources to avoid first-capture stalls (helpful for big/complex trees).
import { preCache } from '@zumer/snapdom';
await preCache({
root: document.body,
embedFonts: true,
localFonts: [{ family: 'Inter', src: '/fonts/Inter.woff2', weight: 400 }],
useProxy: 'your-proxy'
});
SnapDOM maintains internal caches for images, backgrounds, resources, styles, and fonts.
You can control how they are cleared between captures using the cache
option:
Mode | Description |
---|---|
"disabled" |
No cache |
"soft" |
Clears session caches (styleMap , nodeMap , styleCache ) (default) |
"auto" |
Minimal cleanup: only clears transient maps |
"full" |
Keeps all caches (nothing is cleared, maximum performance) |
Examples:
// Use minimal but fast cache
await snapdom.toPng(el, { cache: 'auto' });
// Keep everything in memory between captures
await snapdom.toPng(el, { cache: 'full' });
// Force a full cleanup on every capture
await snapdom.toPng(el, { cache: 'disabled' });
- External images should be CORS-accessible (use
useProxy
option for handling CORS denied) - When WebP format is used on Safari, it will fallback to PNG rendering.
@font-face
CSS rule is well supported, but if need to use JSFontFace()
, see this workaround#43
Setup. Vitest benchmarks on Chromium, repo tests. Hardware may affect results. Values are average capture time (ms) → lower is better.
Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
---|---|---|---|---|
Small (200×100) | 0.5 ms | 0.8 ms | 67.7 ms | 3.1 ms |
Modal (400×300) | 0.5 ms | 0.8 ms | 75.5 ms | 3.6 ms |
Page View (1200×800) | 0.5 ms | 0.8 ms | 114.2 ms | 3.3 ms |
Large Scroll (2000×1500) | 0.5 ms | 0.8 ms | 186.3 ms | 3.2 ms |
Very Large (4000×2000) | 0.5 ms | 0.9 ms | 425.9 ms | 3.3 ms |
Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
---|---|---|---|---|
Small (200×100) | 1.6 ms | 3.3 ms | 68.0 ms | 14.3 ms |
Modal (400×300) | 2.9 ms | 6.8 ms | 87.5 ms | 34.8 ms |
Page View (1200×800) | 17.5 ms | 50.2 ms | 178.0 ms | 429.0 ms |
Large Scroll (2000×1500) | 54.0 ms | 201.8 ms | 735.2 ms | 984.2 ms |
Very Large (4000×2000) | 171.4 ms | 453.7 ms | 1,800.4 ms | 2,611.9 ms |
git clone https://github.com/zumerlab/snapdom.git
cd snapdom
npm install
npm run test:benchmark
Planned improvements for future versions of SnapDOM:
-
Implement plugin system SnapDOM will support external plugins to extend or override internal behavior (e.g. custom node transformers, exporters, or filters).
-
Refactor to modular architecture Internal logic will be split into smaller, focused modules to improve maintainability and code reuse.
-
Decouple internal logic from global options Functions will be redesigned to avoid relying directly on
options
. A centralized capture context will improve clarity, autonomy, and testability. Seenext
branch -
Expose cache control Users will be able to manually clear image and font caches or configure their own caching strategies.
-
Auto font preloading Required fonts will be automatically detected and preloaded before capture, reducing the need for manual
preCache()
calls. -
Document plugin development A full guide will be provided for creating and registering custom SnapDOM plugins.
-
Make export utilities tree-shakeable Export functions like
toPng
,toJpg
,toBlob
, etc. will be restructured into independent modules to support tree shaking and minimal builds.
Have ideas or feature requests? Feel free to share suggestions or feedback in GitHub Discussions.
To contribute or build snapDOM locally:
# Clone the repository
git clone https://github.com/zumerlab/snapdom.git
cd snapdom
# Switch to dev branch
git checkout dev
# Install dependencies
npm install
# Compile the library (ESM, CJS, and minified versions)
npm run compile
# Install playwright browsers (necessary for running tests)
npx playwright install
# Run tests
npm test
# Run Benchmarks
npm run test:benchmark
The main entry point is in src/
, and output bundles are generated in the dist/
folder.
For detailed contribution guidelines, please see CONTRIBUTING.
Special thanks to @megaphonecolin for supporting this project!
If you'd like to support this project too, you can become a sponsor.
MIT © Zumerlab