SnapDOMGitHub★ 8K
Documentation
zumerlab/snapdom …

SnapDOM Plugins

Use plugins to turn a capture into HTML, structured context, element maps, PDFs or recordings. Write hooks when your application needs to change the captured content or add an output.

Build a plugin Browse plugins
What is a SnapDOM plugin

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';
PluginCategoryDescription
timestamp-overlayTransformAdds a configurable timestamp label on the captured clone. Supports multiple date formats and positions.
filterTransformApplies CSS filter effects to captures. Ships with presets: grayscale, sepia, blur, vintage, dramatic.
replace-textTransformFind-and-replace text in the captured clone. Supports strings and regex patterns.
color-tintTransformTints the entire capture to a specified color using an overlay with mix-blend-mode.
redact-inputsTransformMasks 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-exportExportAdds a toAscii() method that converts captures to ASCII art. Configurable width, charset, and luminance, per plugin or per toAscii() call.
pdf-imageExporttoPdfImage() embeds a JPEG in a downloadable, single-page A4 PDF. Supports portrait and landscape.
html-exportExporttoHtml() returns the captured markup, styles and fonts as a self-contained HTML document or fragment.
gif-exportExportAdds a toGif() method that records a sequence of captures into an animated GIF.
video-exportExporttoMp4() records live captures through MediaRecorder, using WebM when MP4 is unavailable.
agent-mapExporttoAgentMap() returns an element map with names, roles, state and bounding boxes, plus an optional raw or annotated image.
context-exportExporttoContext() 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();

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:

  1. Clone the template — npx degit zumerlab/snapdom/packages/plugin-template my-plugin
  2. Write your hook logic — export function myPlugin() {}
  3. 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 }]
  ]
});

Lifecycle hooks

Hooks run in capture order (see the capture flow):

HookStagePurpose
beforeSnapStartAdjust options before any work.
beforeClonePre-cloneBefore DOM clone (modify live DOM carefully).
resolveNodePer nodeReplace or skip a node after the compiled exclusion policy.
afterClonePost-cloneModify cloned tree safely (e.g. inject overlay).
beforeRenderPre-renderRight before the selected render engine runs.
afterRenderPost-renderInspect context.dataURL / context.meta, and svgString on the SVG path.
defineExportsResult setupAfter capture, add custom exporters (e.g. toPdf).
beforeExportPer exportBefore each toPng, toSvg, etc.
afterExportPer exportObserve the returned result.
afterSnapOnceAfter 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:

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:

Ready to build?

Use an existing plugin or start with the template and the hook reference.

Browse plugins Install from npm