SnapDOMGitHub8K
Documentation
zumerlab/snapdom

SnapDOM Plugins

The developer guide to extending SnapDOM. Build a plugin, hook into any stage of the capture pipeline, read the shared context object, and add your own export formats, all without touching the core library.

Build a plugin Browse plugins
What is a SnapDOM plugin

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@2.x.x
import { filter } from '@zumer/snapdom-plugins/filter';
import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay';
PluginCategoryDescription
agent-mapExportSet-of-Mark package for visual agents: toAgentMap() returns an annotated screenshot plus a numbered map of interactive elements with roles, names and bounding boxes.
html-exportExportAdds toHtml(): the capture as a self-contained, re-renderable HTML document instead of pixels.
gif-exportExportAdds toGif(): records the live element over time and encodes an animated GIF in the browser.
video-exportExportAdds toMp4(): records the element with MediaRecorder (MP4 where supported, WebM otherwise).
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.
ascii-exportExportAdds a toAscii() method that converts captures to ASCII art. Configurable width, charset, and luminance.
pdf-imageExportExports the capture as a PNG embedded in a downloadable PDF. Supports portrait and landscape orientations.
html-in-canvasExportUses the experimental WICG drawElement API for direct DOM-to-canvas rendering where supported (Chrome with chrome://flags/#canvas-draw-element).

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:

  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).
afterClonePost-cloneModify cloned tree safely (e.g. inject overlay).
beforeRenderPre-serializeRight before SVG → data URL.
afterRenderPost-serializeInspect context.svgString / context.dataURL.
beforeExportPer exportBefore each toPng, toSvg, etc.
afterExportPer exportObserve the produced result (it cannot replace it).
afterSnapOnceAfter first export; cleanup.
defineExportsSetupAdd custom exporters (e.g. toPdf).
resolveNodePer nodeReplace or skip individual nodes while the clone is built.

Hook order: beforeSnap → beforeClone → afterClone → beforeRender → afterRender → [per export: beforeExport → afterExport] → afterSnap (once, after the first export). What afterExport returns becomes the payload for the next plugin's afterExport; it does not change the value the caller gets back. To produce a different output, add your own export with defineExports.

Context object

There are two context shapes, and mixing them up is the most common plugin bug.

Passing data from the clone phase to an export. Setting context.__myData in afterClone does not reach defineExports: they are different objects. Mirror it on context.options, which is what the export context is spread from:

afterClone(context) {
  const data = collect(context.clone);
  context.__myData = data;                                 // later clone-phase hooks
  if (context.options) context.options.__myData = data;    // export hooks / defineExports
},
defineExports() {
  return { mine: async (ctx) => ctx.__myData };
}

You may safely modify the context (e.g. override backgroundColor or quality), but do so early (beforeSnap, on context.options) 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 exposes a helper named toPdf() on the capture result, plus result.to('pdf'). Bare result.pdf() is not generated.

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, { format, options }) {},

    /**
     * After EACH export call. Observation only: what you return becomes the payload for
     * the next plugin, not the value the caller receives.
     */
    async afterExport(context, { format, options, 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:

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