SnapDOM includes a lightweight plugin system that lets you extend or override behavior at any stage of the capture and export process, without touching the core library. A plugin is a plain object with a unique name and one or more lifecycle hooks. Hooks can be synchronous or async, and they all receive a shared context object. Looking for ready-made plugins to install? See the community plugin showcase.
Official plugins
Install the official plugin package:
npm install @zumer/snapdom-plugins
import { filter } from '@zumer/snapdom-plugins/filter';
import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay';
| Plugin | Category | Description |
|---|---|---|
| picture-resolver | Capture | Resolves lazy-loaded <picture> placeholders. Detects base64 stubs and fetches the real image before capture. |
| timestamp-overlay | Transform | Adds a configurable timestamp label on the captured clone. Supports multiple date formats and positions. |
| filter | Transform | Applies CSS filter effects to captures. Ships with presets: grayscale, sepia, blur, vintage, dramatic. |
| replace-text | Transform | Find-and-replace text in the captured clone. Supports strings and regex patterns. |
| color-tint | Transform | Tints the entire capture to a specified color using an overlay with mix-blend-mode. |
| ascii-export | Export | Adds a toAscii() method that converts captures to ASCII art. Configurable width, charset, and luminance. |
| pdf-image | Export | Exports the capture as a PNG embedded in a downloadable PDF. Supports portrait and landscape orientations. |
| html-in-canvas | Export | Uses the experimental WICG drawElementImage API for direct DOM-to-canvas rendering where supported. |
| prompt-export | Export | LLM-friendly capture: adds a toPrompt() method that returns an annotated screenshot, structured element map with bounding boxes, and a pre-formatted text prompt. |
Community plugins are listed on the plugins showcase page. To submit your plugin, open a PR adding one line to community-plugins.md (see CONTRIBUTING_PLUGINS.md).
Build a plugin in 5 minutes
SnapDOM's hook system gives you full control over every stage of the capture pipeline:
- Clone the template:
npx degit zumerlab/snapdom/packages/plugin-template my-plugin - Write your hook logic:
export function myPlugin() {} - Get listed: open a PR adding one line to
community-plugins.md
See PLUGIN_SPEC.md for the full specification and CONTRIBUTING_PLUGINS.md for submission guidelines.
Registering plugins
Global registration (applies to all captures):
import { snapdom } from '@zumer/snapdom';
// You can register instances, factories, or [factory, options]
snapdom.plugins(
myPluginInstance,
[myPluginFactory, { optionA: true }],
{ plugin: anotherFactory, options: { level: 2 } }
);
Per-capture registration (only for that specific call):
const out = await snapdom(element, {
plugins: [
[overlayFilterPlugin, { color: 'rgba(0,0,0,0.25)' }],
[myFullPlugin, { providePdf: true }]
]
});
- Execution order = registration order (first registered, first executed).
- Per-capture plugins run before global ones.
- Duplicates are automatically skipped by
name; a per-capture plugin with the samenameoverrides its global version.
Lifecycle hooks
Hooks run in capture order (see the capture flow):
| Hook | Stage | Purpose |
|---|---|---|
| beforeSnap | Start | Adjust options before any work. |
| beforeClone | Pre-clone | Before DOM clone (modify live DOM carefully). |
| afterClone | Post-clone | Modify cloned tree safely (e.g. inject overlay). |
| beforeRender | Pre-serialize | Right before SVG → data URL. |
| afterRender | Post-serialize | Inspect context.svgString / context.dataURL. |
| beforeExport | Per export | Before each toPng, toSvg, etc. |
| afterExport | Per export | Transform returned result. |
| afterSnap | Once | After first export; cleanup. |
| defineExports | Setup | Add custom exporters (e.g. toPdf). |
Hook order: beforeSnap → beforeClone → afterClone → beforeRender → afterRender → beforeExport → afterExport (with afterSnap firing once after the first export). Returned values from afterExport are chained to the next plugin (transform pipeline).
Context object
Every hook receives a single context object that contains normalized capture state:
- Input & options:
element,debug,fast,scale,dpr,width,height,backgroundColor,quality,useProxy,cache,outerTransforms,outerShadows,compress,embedFonts,localFonts,iconFonts,excludeFonts,exclude,excludeMode,filter,filterMode,fallbackURL. - Intermediate values (depending on stage):
clone,classCSS,styleCache,fontsCSS,baseCSS,svgString,dataURL. - During export:
context.export = { type, options, url }wheretypeis the exporter name ("png","jpeg","svg","blob", etc.), andurlis the serialized SVG base.
You may safely modify context (e.g. override backgroundColor or quality), but do so early (beforeSnap) for global effects or in beforeExport for single-export changes.
Custom exports via plugins
Plugins can add new exports using defineExports(context). For each export key you return (e.g. "pdf"), SnapDOM automatically exposes a helper method named toPdf() on the capture result.
Register the plugin (global or per capture):
import { snapdom } from '@zumer/snapdom';
// global
snapdom.plugins(pdfExportPlugin());
// or per capture
const out = await snapdom(element, { plugins: [pdfExportPlugin()] });
Call the custom export:
const out = await snapdom(document.querySelector('#report'));
// because the plugin returns { pdf: async (ctx, opts) => ... }
const pdfBlob = await out.toPdf({
// exporter-specific options (width, height, quality, filename, etc.)
});
Example: overlay filter plugin
Adds a translucent overlay or color filter only to the captured clone (not your live DOM). Useful for highlighting or dimming sections before export.
/**
* Ultra-simple overlay filter for SnapDOM (HTML-only).
* Inserts a full-size <div> overlay on the cloned root.
*
* @param {{ color?: string; blur?: number }} [options]
* color: overlay color (rgba/hex/hsl). Default: 'rgba(0,0,0,0.25)'
* blur: optional blur in px (default: 0)
*/
export function overlayFilterPlugin(options = {}) {
const color = options.color ?? 'rgba(0,0,0,0.25)';
const blur = Math.max(0, options.blur ?? 0);
return {
name: 'overlay-filter',
/**
* Add a full-coverage overlay to the cloned HTML root.
* @param {any} context
*/
async afterClone(context) {
const root = context.clone;
if (!(root instanceof HTMLElement)) return; // HTML-only
// Ensure containing block so absolute overlay anchors to the root
if (getComputedStyle(root).position === 'static') {
root.style.position = 'relative';
}
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.left = '0';
overlay.style.top = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.background = color;
overlay.style.pointerEvents = 'none';
if (blur) overlay.style.filter = `blur(${blur}px)`;
root.appendChild(overlay);
}
};
}
Usage:
import { snapdom } from '@zumer/snapdom';
// Global registration
snapdom.plugins([overlayFilterPlugin, { color: 'rgba(0,0,0,0.3)', blur: 2 }]);
// Per-capture
const out = await snapdom(document.querySelector('#card'), {
plugins: [[overlayFilterPlugin, { color: 'rgba(255,200,0,0.15)' }]]
});
const png = await out.toPng();
document.body.appendChild(png);
The overlay is injected only in the cloned tree, never in your live DOM, ensuring perfect fidelity and zero flicker.
Full plugin template
Use this as a starting point for custom logic or exporters.
export function myPlugin(options = {}) {
return {
/** Unique name used for de-duplication/overrides */
name: 'my-plugin',
/** Early adjustments before any clone/style work. */
async beforeSnap(context) {},
/** Before subtree cloning (use sparingly if touching the live DOM). */
async beforeClone(context) {},
/** After subtree cloning (safe to modify the cloned tree). */
async afterClone(context) {},
/** Right before serialization (SVG/dataURL). */
async beforeRender(context) {},
/** After serialization; inspect context.svgString/context.dataURL if needed. */
async afterRender(context) {},
/** Before EACH export call (toPng/toSvg/toBlob/...). */
async beforeExport(context) {},
/**
* After EACH export call.
* If you return a value, it becomes the result for the next plugin (chaining).
*/
async afterExport(context, result) { return result; },
/**
* Define custom exporters (auto-added as helpers like out.toPdf()).
* Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
*/
async defineExports(context) { return {}; },
/** Runs ONCE after the FIRST export finishes (cleanup). */
async afterSnap(context) {}
};
}
Quick recap:
- Plugins can modify capture behavior (
beforeSnap,afterClone, etc.). - You can inject visuals or transformations safely into the cloned tree.
- New exporters defined in
defineExports()automatically become helpers likeout.toPdf(). - All hooks can be asynchronous, run in order, and share the same
context.
Start with the plugin template
Start from the plugin template, add the hook you need and test it against a real capture.
Browse plugins Install from npm