Domotion

elementTreeToSvg()

Convert a captured tree into a self-contained SVG string.

Pure function: takes a captured element tree (no Playwright, no I/O) and returns the SVG markup. Always paired with captureElementTree().

Signature

function elementTreeToSvg(
  elements: CapturedElement[],
  width: number,
  height: number,
  idPrefix?: string,
  includeGlyphDefs?: boolean,
): string

Parameters

elements
CapturedElement[]
A tree from captureElementTree (or one you've programmatically built / mutated).
width
number
SVG viewBox and width attribute. Content beyond this is clipped.
height
number
SVG viewBox and height attribute.
idPrefix
string (default "")
Prefix added to every clip-path, gradient, and glyph ID generated for this SVG. Required when you'll be combining several rendered SVGs into one (e.g. animation frames) so IDs from different captures don't collide.
includeGlyphDefs
boolean (default true)
When true, the returned markup includes a <defs> with every glyph path used in this SVG. Set to false when you'll hoist shared glyph defs to an outer SVG (e.g. a multi-frame animated SVG).

Returns

An SVG fragment: a <defs> block (when defs are present) followed by the rendered element groups. The function does not wrap the output in an outer <svg> root or an XML declaration — that's left to the caller, because Domotion's own animated-SVG composer (generateAnimatedSvg()) needs to combine fragments from multiple captures into one root SVG.

To get a standalone, browser-loadable SVG, wrap the fragment with the wrapSvg() helper:

import { elementTreeToSvg, wrapSvg } from "domotion";

const svg = wrapSvg(elementTreeToSvg(tree, w, h), w, h);

Then run it through optimizeSvg() for production size. The output is human-readable and diff-friendly.

wrapSvg(inner, width, height)

One-liner that wraps an inner SVG fragment in the standard <svg xmlns="..." viewBox="0 0 W H" width="W" height="H">...</svg> shell. Use it for every standalone capture; generateAnimatedSvg() already handles wrapping internally.

Examples

Standalone capture

const tree = await captureElementTree(page, "body", vp);
const svg  = elementTreeToSvg(tree, vp.width, vp.height);
writeFileSync("capture.svg", svg);

Multiple frames sharing one SVG

const frames = [];
for (let i = 0; i < 3; i++) {
  await page.setContent(buildHtml(i));
  const tree = await captureElementTree(page, "body", vp);
  const svgContent = elementTreeToSvg(
    tree, vp.width, vp.height,
    `f${i}-`,   // idPrefix prevents ID collisions across frames
  );
  frames.push({ svgContent, duration: 1500 });
}
const animated = generateAnimatedSvg({ width: vp.width, height: vp.height, frames });

Pitfalls

  • Forgetting idPrefix for multi-frame captures. Without unique prefixes, two frames will both define <clipPath id="clip0"> and the SVG will render the wrong geometry on later frames.
  • Mismatch between viewport and SVG size. The renderer paints at the captured coordinates and clips at width / height. To fit a smaller region, capture with a tight viewport box first; don't try to scale at the SVG level.

See also