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

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 via assignedNodes({ 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 placeholders is 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 the poster; honors object-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, :invalid and :in-range styles render. ::placeholder color is preserved. Firefox checkboxes/radios get a drawn replacement.
  • <img> โ€” srcset is frozen, pre-transform dimensions recorded, px sizes frozen when the author used %/auto, and object-fit / object-position preserved.
  • 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 so var() 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 !important still 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-element content.
  • -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/scale are read into a total matrix with origin-aware bounding-box math (see the outerTransforms option).
  • Shadows, blur & outline bleed โ€” box-shadow, filter blur, drop-shadow and outline expand the viewBox when outerShadows is on; otherwise root shadows are visually stripped (see outerShadows).
  • Masks, backgrounds & border-image โ€” see Images & backgrounds.
  • Custom scrollbars โ€” ::-webkit-scrollbar rules 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 โ€” resolves currentSrc/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 in background-image (and the background shorthand), mask / -webkit-mask*, and border-image, preserving multi-layer values and layout longhands (position, size, repeat, origin, clip, blend-mode, compositeโ€ฆ). Supports background-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. useProxy accepts flexible templates ({url}, {urlRaw}, ?url= suffix, and more); already-proxied and data:/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; set compress: false to embed verbatim.
  • image-set() / -webkit-image-set() โ€” in background-image and pseudo-element content, the candidate matching the live device pixel ratio is inlined (not just whichever url() 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-face embedding (embedFonts) โ€” scans document (and iframe) stylesheets and embeds only the @font-face rules 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 iconFonts option or window.__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 to embedFonts: true and cache: 'full'.

Export formats

A snapdom(el) call returns a reusable result object; capture once, export many times.

MethodReturns
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.

OptionDefaultBehavior
debugfalseDebug warnings
fasttrueSkip idle delay for speed
scale1Output scale multiplier
exclude[]CSS selectors to exclude
excludeMode'hide''hide' (spacer) or 'remove'
filternullNode predicate (node) => boolean
filterMode'hide''hide' or 'remove'
placeholderstrueShow placeholders for failed images / cross-origin iframes
embedFontsfalseEmbed matched @font-face
iconFonts[]Extra icon-font names/regexes
localFonts[]User font descriptors
excludeFontsundefined{ families, domains, subsets }
fontStylesheetDomains[]Extra cross-origin CSS domains
fallbackURLundefinedFallback image URL or callback
cache'soft'disabled / soft / auto / full
useProxy''CORS proxy template/base
widthnullOutput width (aspect-preserving)
heightnullOutput height
format'png'png / jpgโ†’jpeg / webp / svg
type'svg'Output type svg / img / canvas / blob
quality0.92Lossy encode quality
dprdevicePixelRatio || 1Device pixel ratio
backgroundColornull (#ffffff for jpeg/webp)Flatten background
filename'snapDOM'Download filename base
outerTransformstrueNormalize root translate/rotate vs. expand bbox for transforms
outerShadowsfalseStrip root shadows vs. expand bleed for shadows/blur/outline
compresstruePerceptual raster downsampling
reconcilefalseMeasure the clone against the live DOM and pin diverging boxes to their real size (roughly doubles capture time)
burstfalseMemoize repeated captures of an unchanged element via a scoped MutationObserver, skipping the pipeline entirely
invalidatefalseWith burst: true, force a fresh capture for changes automatic tracking can't see (canvas draws, programmatic CSSOM edits)
safariWarmupAttempts3Safari warmup iterations (1โ€“3)
excludeStylePropsnullRegExp/predicate to skip style props
resolvePicturePlaceholderstrueBuilt-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 defineExports can add or override export formats; each becomes a to<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, baseStyle and defaultStyle; WeakMaps for computed styles and layout measurement hints; a Set for fonts; and a per-session bucket.
  • Policies (cache option):
    • 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 font loadingdone/ready events bump a style-snapshot epoch, so stale snapshots are dropped automatically.
  • preCache โ€” warm the caches ahead of time (defaults to cache: 'full').
  • burst โ€” memoizes repeated captures of an unchanged element: a scoped MutationObserver (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 with invalidate: true to 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 + drawImage to prime the pipeline when fonts are embedded or the element has background/mask/canvas content. Configurable via safariWarmupAttempts.
  • 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 (per excludeMode).
  • 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