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 — 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
import { filter } from '@zumer/snapdom-plugins/filter';
import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay';
PluginCategoryDescription
picture-resolverCaptureResolves lazy-loaded <picture> placeholders. Detects base64 stubs and fetches the real image before capture.
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 drawElementImage API for direct DOM-to-canvas rendering where supported.
prompt-exportExportLLM-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:

  1. Clone the templatenpx degit zumerlab/snapdom/packages/plugin-template my-plugin
  2. Write your hook logicexport 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 exportTransform returned result.
afterSnapOnceAfter first export; cleanup.
defineExportsSetupAdd 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:

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:

Ready to build?

Clone the plugin template, write a hook, and ship a SnapDOM plugin in minutes.

Browse plugins Install from npm