SnapDOM
July 24, 2026 ยท View on GitHub
A complete technical overview of what SnapDOM captures, embeds and exports. SnapDOM serializes a DOM subtree into a self-contained SVG (via <foreignObject>) and rasterizes it to your target format โ ultra-fast, dependency-free, and 100% based on standard Web APIs.
๐ Full API, options and guides: snapdom.dev/docs
๐ ็ฎไฝไธญๆ: FEATURES_CN.md
Table of Contents
- Capture & clone
- Styles
- Images & backgrounds
- Fonts & icon fonts
- Export formats
- Options
- Plugin system
- Caching & preCache
- Cross-browser handling
- Node-level control attributes
Capture & clone
Deep node-by-node clone that snapshots the computed style of every node, preserving what the browser actually renders.
- Shadow DOM โ traverses
shadowRoot, extracts and scopes its CSS, seeds the required CSS custom properties, and resolves<slot>content viaassignedNodes({ flatten: true })(slotted subtrees are marked to avoid double-cloning). - Same-origin iframes โ rasterized inline (fonts read from the iframe's own document). Cross-origin iframes can't be read, so they render as a striped placeholder (or a hidden spacer when
placeholdersis off). <canvas>โ snapshotted to a PNG<img>(with Safari-safe retries), preserving intrinsic and CSS box size.<video>โ current frame drawn to an image; falls back to theposter; honorsobject-fit: contain.<audio controls>โ replaced with a drawn player mock sized to the element.- Form control state โ
<input>value/checked/indeterminate,<textarea>value, and<select>selection are preserved. State attributes (disabled,required,readonly,min,max,pattern,aria-invalid) are copied so:disabled,:required,:read-only,:invalidand:in-rangestyles render.::placeholdercolor is preserved. Firefox checkboxes/radios get a drawn replacement. <img>โsrcsetis frozen, pre-transform dimensions recorded, px sizes frozen when the author used%/auto, andobject-fit/object-positionpreserved.- SVG โ paint properties (fill, stroke and its longhands, opacity variants, fill/clip rule, markers, visibility, display) copied as inline styles; external
<defs>/<symbol>referenced by<use>are inlined sovar()resolves at the use site. - Scroll position โ scrolled containers are reproduced by translating their inner content and clipping overflow; fixed/absolute descendants are adjusted, and sticky headers/footers are frozen in place.
- Skipped by design โ
meta,script,noscript,title,link,template, the SnapDOM sandbox, and nested<foreignObject>.
Non-renderable content is handled gracefully: invalid XML control characters are stripped, content-visibility is forced visible so off-screen content is captured, and root margins are neutralized.
Styles
- Computed-style inlining โ every node's full computed style is snapshotted and deduplicated into generated CSS classes to keep output compact. Authored inline styles are replaced with computed values so stylesheet
!importantstill wins. - Preserved details โ text-decoration longhands (line/color/style/thickness, underline-offset, skip-ink),
-webkit-text-stroke+paint-order, and (when embedding fonts) font-feature/variation/kerning/variant/optical-sizing settings. counter()/counters()โ a full CSS counter resolver (counter-reset with nesting, counter-increment, counter-set, and counter-style formatting), used in pseudo-elementcontent.-webkit-line-clamp&text-overflow: ellipsisโ baked into real text (withโฆ) because Firefox and Safari don't honor them inside<foreignObject>.- Transforms โ base and individual
translate/rotate/scaleare read into a total matrix with origin-aware bounding-box math (see theouterTransformsoption). - Shadows, blur & outline bleed โ box-shadow, filter blur, drop-shadow and outline expand the viewBox when
outerShadowsis on; otherwise root shadows are visually stripped (seeouterShadows). - Masks, backgrounds & border-image โ see Images & backgrounds.
- Custom scrollbars โ
::-webkit-scrollbarrules are injected so custom scrollbar styling appears. excludeStylePropsโ skip properties from the snapshot by RegExp or predicate (e.g. drop all CSS variables).reconcileโ mounts the styled clone off-screen, measures every node against its live counterpart, and pins only the boxes whose size diverges to their real value. Fixes rare text re-wrap/layout drift at the cost of roughly doubling capture time; off by default. snapdom warns once (console.warn) if it detects a capture that used width-softening on elements known to sometimes need it.
Images & backgrounds
<img>inlining โ resolvescurrentSrc/src, fetches to a data URL, caches, and ensures dimensions (batched to respect HTTP/1.1 connection limits). SVG<image href>is inlined too.- Backgrounds & masks โ inlines
url()layers inbackground-image(and thebackgroundshorthand),mask/-webkit-mask*, andborder-image, preserving multi-layer values and layout longhands (position, size, repeat, origin, clip, blend-mode, compositeโฆ). Supportsbackground-clip: text. <picture>& lazy images โ resolves<picture>sources and common lazy attributes (data-src,data-lazy-src,data-original,data-hi-res-src,data-srcset, โฆ) to real URLs before cloning.- CORS / proxy โ a non-throwing fetch layer with in-flight deduplication, an error cache, timeouts, and inferred credentials.
useProxyaccepts flexible templates ({url},{urlRaw},?url=suffix, and more); already-proxied anddata:/blob:URLs are skipped. - Failure fallbacks โ a configurable
fallbackURL(string or callback), then a placeholder box, then a hidden spacer. compressโ perceptual downsampling of inlined rasters to their visible resolution (display box ร scale ร dpr), preserving the source codec and never upscaling. On by default; setcompress: falseto embed verbatim.image-set()/-webkit-image-set()โ inbackground-imageand pseudo-elementcontent, the candidate matching the live device pixel ratio is inlined (not just whicheverurl()appears first).- Decode-size guard โ SVG raster size is clamped to safe limits (max 16384px per side, ~268M px area) and downscaled with a warning if exceeded.
Fonts & icon fonts
@font-faceembedding (embedFonts) โ scans document (and iframe) stylesheets and embeds only the@font-facerules for the families/weights/styles/stretch actually used, intersected with the used unicode codepoints, keeping payloads small. Supports near-weight matching and synthetic-italic fallback.- Icon fonts โ auto-detects Font Awesome, Material Icons / Symbols, Ionicons, Glyphicons, Feather, Bootstrap Icons, Remix, Heroicons, Layui and Lucide (plus a heuristic), and renders glyphs (including ligature icons) to images. Extend detection via the
iconFontsoption orwindow.__SNAPDOM_ICON_FONTS__. localFontsโ supply your own fonts as{ family, src, weight?, style?, stretchPct? }to fetch and embed.excludeFontsโ exclude by{ families?, domains?, subsets? }.- Cross-origin stylesheets โ gated by
fontStylesheetDomains(plus known math libraries like KaTeX/MathJax). preCacheโ preloads images, background images and fonts before capture; defaults toembedFonts: trueandcache: 'full'.
Export formats
A snapdom(el) call returns a reusable result object; capture once, export many times.
| Method | Returns |
|---|---|
toRaw() | Raw data:image/svg+xml URL |
toSvg() / toImg() | SVG HTMLImageElement |
toCanvas() | HTMLCanvasElement |
toBlob() | Blob (SVG text blob or rasterized) |
toPng() | PNG image |
toJpg() | JPG image (white background) |
toWebp() | WebP image |
download() | Triggers a file download |
The same methods exist as one-shot static shortcuts (snapdom.toPng(el), snapdom.download(el), โฆ). Lossy formats (JPEG/WebP) auto-flatten transparency to white. Downloads use the Web Share API on iOS. Exports run through a serial per-session queue with beforeExport / afterExport / afterSnap hooks.
Options
Defaults as normalized in src/core/context.js.
| Option | Default | Behavior |
|---|---|---|
debug | false | Debug warnings |
fast | true | Skip idle delay for speed |
scale | 1 | Output scale multiplier |
exclude | [] | CSS selectors to exclude |
excludeMode | 'hide' | 'hide' (spacer) or 'remove' |
filter | null | Node predicate (node) => boolean |
filterMode | 'hide' | 'hide' or 'remove' |
placeholders | true | Show placeholders for failed images / cross-origin iframes |
embedFonts | false | Embed matched @font-face |
iconFonts | [] | Extra icon-font names/regexes |
localFonts | [] | User font descriptors |
excludeFonts | undefined | { families, domains, subsets } |
fontStylesheetDomains | [] | Extra cross-origin CSS domains |
fallbackURL | undefined | Fallback image URL or callback |
cache | 'soft' | disabled / soft / auto / full |
useProxy | '' | CORS proxy template/base |
width | null | Output width (aspect-preserving) |
height | null | Output height |
format | 'png' | png / jpgโjpeg / webp / svg |
type | 'svg' | Output type svg / img / canvas / blob |
quality | 0.92 | Lossy encode quality |
dpr | devicePixelRatio || 1 | Device pixel ratio |
backgroundColor | null (#ffffff for jpeg/webp) | Flatten background |
filename | 'snapDOM' | Download filename base |
outerTransforms | true | Normalize root translate/rotate vs. expand bbox for transforms |
outerShadows | false | Strip root shadows vs. expand bleed for shadows/blur/outline |
compress | true | Perceptual raster downsampling |
reconcile | false | Measure the clone against the live DOM and pin diverging boxes to their real size (roughly doubles capture time) |
burst | false | Memoize repeated captures of an unchanged element via a scoped MutationObserver, skipping the pipeline entirely |
invalidate | false | With burst: true, force a fresh capture for changes automatic tracking can't see (canvas draws, programmatic CSSOM edits) |
safariWarmupAttempts | 3 | Safari warmup iterations (1โ3) |
excludeStyleProps | null | RegExp/predicate to skip style props |
resolvePicturePlaceholders | true | Built-in <picture> / lazy resolver |
pictureResolver | {} | { timeout, concurrency, resolveLazySrc, silent } |
plugins | โ | Per-capture plugin list (local-first) |
Plugin system
Plugins are plain objects with lifecycle hooks, registered globally (snapdom.plugins(...), deduped by name) or per-capture ({ plugins: [...] }, where locals override globals by name).
- Hooks (in order):
beforeSnap โ beforeClone โ afterClone โ beforeRender โ afterRender โ beforeExport โ afterExport โ afterSnap. - Custom exporters โ a plugin's
defineExportscan add or override export formats; each becomes ato<Name>()helper on the result object and gets the same export pipeline as core formats. - Accepted forms โ plain object,
[factory, options],{ plugin, options }, or a factory function.
See PLUGIN_SPEC.md and CONTRIBUTING_PLUGINS.md.
Caching & preCache
- Buckets โ FIFO evicting maps for
image,background,resource,baseStyleanddefaultStyle;WeakMaps for computed styles and layout measurement hints; aSetfor fonts; and a per-session bucket. - Policies (
cacheoption):disabledโ clear all caches every capture.soft(default) โ reset session style/node maps, keep persistent caches.autoโ reset style/node maps only, keep the style cache too.fullโ keep everything.
- Invalidation โ a MutationObserver on the DOM and
<head>plus fontloadingdone/readyevents bump a style-snapshot epoch, so stale snapshots are dropped automatically. preCacheโ warm the caches ahead of time (defaults tocache: 'full').burstโ memoizes repeated captures of an unchanged element: a scopedMutationObserver(plus<video>frame tracking) marks it dirty on external change, and an unchanged repeat capture skips the pipeline entirely. Opt-in; without it, snapdom warns once if the same element is captured 3+ times within 2s. Pair withinvalidate: trueto force a fresh capture after changes automatic tracking can't see (canvas draws, programmatic CSSOM edits).
Cross-browser handling
- Safari warmup โ works around WebKit #219770 (the first canvas draw with an embedded-font SVG is blank) by running small pre-captures +
drawImageto prime the pipeline when fonts are embedded or the element has background/mask/canvas content. Configurable viasafariWarmupAttempts. - Safari canvas โ box-shadow is rewritten to an SVG drop-shadow, and image compositing is awaited before drawing.
- Firefox โ checkboxes and radios get drawn replacements.
- iOS โ
download()falls back to the Web Share API.
Node-level control attributes
Fine-grained control directly in your markup:
data-capture="exclude"โ drop this node (perexcludeMode).data-capture="placeholder"+data-placeholder-textโ render a placeholder box instead of the node.data-snapdom-sandbox/#snapdom-sandboxโ skipped entirely.
Want the full API and every option with examples? โ snapdom.dev/docs