A plugin is a plain object with a unique name and lifecycle hooks. Capture hooks can change the cloned content; defineExports adds methods to the result. Hooks may be synchronous or async. The core already exports images, canvas and blobs; the plugin package supplies the additional outputs below.
Official plugins
Install the official plugin package; use the version that matches your core major version:
npm install @zumer/snapdom-plugins@latest
import { filter } from '@zumer/snapdom-plugins/filter';
import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay';
| Plugin | Category | Description |
|---|---|---|
| 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. |
| redact-inputs | Transform | Masks input and textarea values, excludes private blocks and removes named attributes from captured images, HTML and structured outputs. <select> values are not masked; hide them with a blocks rule. Redaction options and limits. |
| ascii-export | Export | Adds a toAscii() method that converts captures to ASCII art. Configurable width, charset, and luminance, per plugin or per toAscii() call. |
| pdf-image | Export | toPdfImage() embeds a JPEG in a downloadable, single-page A4 PDF. Supports portrait and landscape. |
| html-export | Export | toHtml() returns the captured markup, styles and fonts as a self-contained HTML document or fragment. |
| gif-export | Export | Adds a toGif() method that records a sequence of captures into an animated GIF. |
| video-export | Export | toMp4() records live captures through MediaRecorder, using WebM when MP4 is unavailable. |
| agent-map | Export | toAgentMap() returns an element map with names, roles, state and bounding boxes, plus an optional raw or annotated image. |
| context-export | Export | toContext() returns the captured UI as a text outline or JSON tree. |
HTML export preserves source markup, including event-handler attributes; treat it as untrusted when serving it. GIF and video exports start a new sequence of live captures when called. Community plugins and submission details are on the plugins page and in CONTRIBUTING_PLUGINS.md.
Redact fields, blocks and attributes
Core already masks passwords to match their visible bullets. Use redactInputs() to choose additional content to hide. The live page stays unchanged; selectors match source elements, including nodes in open shadow trees.
import { redactInputs, htmlExport, contextExport } from '@zumer/snapdom-plugins';
const capture = await snapdom(element, {
plugins: [
redactInputs({
all: true,
blocks: ['.private-panel', '[data-private-block]'],
attributes: [
{ selector: '[data-token]', names: ['data-token'] },
{ selector: '.customer', names: ['title', 'aria-label'] }
]
}),
htmlExport(),
contextExport({ format: 'json' })
]
});
const image = await capture.toPng();
const html = await capture.toHtml();
const context = await capture.toContext();
- Fields:
types,autocompleteandselectorselect inputs and textareas;all: trueselects every input and textarea. The extraselectordoes not hide other element types. Defaults cover email, tel,cc-*, current-password, new-password and one-time-code. The mask uses same-length bullets; custommask(value, element)can return another string. Different glyph widths can change wrapping. - Blocks:
blocksaccepts one CSS selector or an array, and combines withexclude. The defaultexcludeMode: 'hide'keeps invisible space;'remove'drops the subtree from the captured layout. Removing a blocked capture root throws; use'hide'for that capture. - Attributes: each rule removes the exact names listed in
names, including their projections in structured exports. Names are not wildcard patterns. Avaluerule on an input or textarea clears its displayed value and omits structuredstate.value.
HTML uses the sanitized clone. agentMap and contextExport apply the same field, block and attribute rules to source-derived data, regardless of official plugin order. Removing an attribute does not erase copies already present in visible text, CSS content or bitmap pixels; use blocks for the subtree that paints that content. These rules do not search arbitrary text or images for sensitive information.
Custom masks and nonempty selector, blocks or attributes rules run on every capture instead of reusing an unchanged capture memo. Nonempty block or attribute rules add a final beforeRender pass, which makes the experimental html-in-canvas engine fall back to SVG. Field-only redaction does not add that pass. See the complete plugin reference for defaults and examples.
Build a plugin
Start with the template and keep only the hooks your plugin uses:
- 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). |
| resolveNode | Per node | Replace or skip a node after the compiled exclusion policy. |
| afterClone | Post-clone | Modify cloned tree safely (e.g. inject overlay). |
| beforeRender | Pre-render | Right before the selected render engine runs. |
| afterRender | Post-render | Inspect context.dataURL / context.meta, and svgString on the SVG path. |
| defineExports | Result setup | After capture, add custom exporters (e.g. toPdf). |
| beforeExport | Per export | Before each toPng, toSvg, etc. |
| afterExport | Per export | Observe the returned result. |
| afterSnap | Once | After the first successful export; cleanup. |
Order: beforeSnap → beforeClone → resolveNode (per node) → afterClone → beforeRender → afterRender → defineExports → [beforeExport → exporter → afterExport] → afterSnap. The bracketed segment repeats per export; afterSnap fires once after the first successful one. Export-hook return values are ignored. Change an export's options in beforeExport, or use defineExports to produce a custom result.
v2.x.x migration: an afterExport return used to become the next hook's payload; it never replaced the output returned to the caller. V3 ignores those returns and gives hooks the same export payload. Remove return-value chaining. The TypeScript name PluginExportFacade is also removed, but ctx.exports remains available inside defineExports. Infer its type there or use NonNullable<CaptureContext['exports']> after importing the CaptureContext type.
Context object
Capture hooks share one context containing normalized options and stage results. Export hooks receive a view for that export:
- Input & normalized options:
element; theoptionsself-reference;needs;shouldExclude(element); and the public capture options such asscale,dpr,width,height,format,backgroundColor,quality, fonts, exclusion, clipping, selection, reconciliation, invalidation, caching, plugins, and engine.formatis canonical; deprecatedtypestays synchronized with it, and changing either inbeforeSnapis honored. - Intermediate values (depending on stage):
clone,classCSS,styleCache,nodeMap,fontsCSS,baseCSS,svgString,dataURL, and frozenmeta. Large clone-stage fields are released after rendering. - Result setup:
artifacts = { classCSS, fontsCSS, baseCSS, scrollbarCSS }andexports, the hook-free core exporter facade available todefineExports. - During export:
context.export = { type, options, requestedOptions, url, svgString }.typeis the exporter name; frozenrequestedOptionspreserves exactly what the caller supplied.optionsis the merged option bag passed to the exporter.urlis the captured data URL: SVG normally, PNG after a successful native capture.svgString()lazily decodes an SVG capture and throws for a raster capture.
Change capture options in beforeSnap. The exceptions are plugins, needs, invalidate and cache, which have already been resolved. In beforeExport(ctx, payload), mutate payload.options; changing the context view does not steer that export.
Pipeline depth and memoization
A per-capture plugin may declare needs: "clone" or needs: "render" (the default). SnapDOM runs to the deepest requested stage. A clone-only result can expose plugin outputs such as context or an element map, but reading url or calling an image exporter throws. It does not recapture on demand. Global plugins must use "render".
Clone/render-affecting hooks suspend automatic repeat memoization unless the plugin declares pure: true. Purity restores unchanged-repeat memoization. Pure beforeRender/afterRender hooks may also use differential recapture, while hooks involved in clone construction still force a conservative full recapture after a change. A capture stopped at needs: "clone" is never memoized because it has no render artifact. Declare purity only for deterministic, idempotent hooks; export-only hooks and defineExports do not need it.
context.shouldExclude(node) covers both exclude and filter, following updates made in beforeSnap. Content selection checks data-capture="exclude", then exclude, then filter; the first omission decides its independent excludeMode or filterMode. resolveNode(node, context) runs after those checks. Return a replacement Node, null to skip, or undefined to continue normal cloning.
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.
Use several outputs from one capture:
import { snapdom } from '@zumer/snapdom';
import { htmlExport, contextExport } from '@zumer/snapdom-plugins';
const out = await snapdom(element, {
plugins: [htmlExport(), contextExport()]
});
Call the custom export:
const png = await out.toPng();
const html = await out.toHtml();
const context = await out.toContext({ format: 'json' });
Example: overlay filter plugin
This example dims the captured HTML with a translucent overlay. It changes the clone while leaving the live page untouched.
/**
* Overlay filter for captured HTML.
* 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(context.element).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 source element supplies the live position value because the clone is detached. The overlay itself is added only to the clone.
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',
/** Optional pipeline depth; global plugins must remain at "render". */
needs: 'render',
/** Set true only for deterministic/idempotent clone or render hooks. */
pure: false,
/** Early adjustments before any clone/style work. */
async beforeSnap(context) {},
/** Before subtree cloning (use sparingly if touching the live DOM). */
async beforeClone(context) {},
/** Per source node, after context.shouldExclude(node) has been applied. */
async resolveNode(node, context) {},
/** After subtree cloning (safe to modify the cloned tree). */
async afterClone(context) {},
/** Right before the selected renderer runs. */
async beforeRender(context) {},
/** After rendering; svgString exists only on the SVG path. */
async afterRender(context) {},
/**
* Define custom exporters during result setup.
* Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
*/
async defineExports(context) { return {}; },
/** Before EACH export call (toPng/toSvg/toBlob/...). */
async beforeExport(context, { format, options }) {},
/**
* After EACH export call.
* Observes { format, options, result }; return values are ignored.
*/
async afterExport(context, { format, options, result }) {},
/** Runs ONCE after the FIRST successful export finishes (cleanup). */
async afterSnap(context) {}
};
}
Before using a plugin in production:
- Keep frozen data on the capture context, not a shared plugin closure.
- Test repeated exports and concurrent captures using the same plugin instance.
- Use the core
exportsfacade fromdefineExportsfor raster work, so sizing, compressed originals and Safari handling stay consistent. - Test in Chromium, Firefox and Safari, including
scale: 2and changes after the first capture.
Ready to build?
Use an existing plugin or start with the template and the hook reference.
Browse plugins Install from npm