Skip to content
SnapDOMTry SnapStudioGitHub★ 8K
Official plugin · MIT

Export editable vector artwork.

Reconstruct supported text and shapes as SVG artwork or copy the captured result into Figma.

Getting started

Requires SnapDOM 3.4.0 or newer within 3.x. Source and MIT license.

npm install @zumer/snapdom @zumer/snapdom-vector
import { snapdom } from '@zumer/snapdom'
import vector from '@zumer/snapdom-vector'

const shot = await snapdom(document.querySelector('#plan-card'), {
  plugins: [vector()]
})

const svg = await shot.toVector()   // a standalone SVG string

const copyButton = document.querySelector('#copy-to-figma')
copyButton.addEventListener('click', () => shot.toFigma()) // HTTPS or localhost

The exporter builds the artwork in the browser. It does not upload the capture; SnapDOM may fetch images and fonts used by the page.

Fidelity

See what changes in each format

Browsers, SVG and Figma do not support the same effects. The plugin tells you when it had to adapt, rasterize or omit something, and identifies the affected layer. You can review those changes before handing the file to a designer or a client.

Exact
The destination can reproduce the layer as editable vector content.
Adapted
The layer remains vector, but an effect was translated to the closest available equivalent.
Rasterized or omitted
The effect has no usable vector equivalent. The report names the layer and explains what happened.

The clearest case is backdrop-filter: frosted glass is a blur of whatever sits behind the panel, and SVG has no way to express that, so the panel arrives as a flat colour. The report names the layer, and the limits list it.

Read about fidelity reports

How it works

Built from the browser's resolved layout

Grid, flexbox, sticky positioning and container queries are already resolved on screen. The plugin measures that result instead of trying to implement another CSS layout engine.

Editable output
Text remains text. Gradients, borders, radii, shadows, transforms, images and DOM hierarchy become vector paint and usable layers.
Browser layout
Boxes, text lines and baselines are measured from the rendered page, including grid, flexbox, pseudo-elements and open shadow DOM.
Private by design
The plugin runs in the browser, including authenticated and staging pages. Captures are not uploaded to a server.
Tested output
Fixtures are rendered and compared against captures of the live elements in Chromium.

Registering

Register it per capture to avoid running the vector hook on unrelated image exports:

// This capture only
await snapdom(el, { plugins: [vector()] })

// every capture in the page
snapdom.plugins(vector())

The vector hook suspends automatic capture memoization because it needs the resolved clone. Global registration applies that work to every capture, including PNG exports.

Keep the source element connected and laid out until export finishes. An element with display: none has no layout box to measure and causes an error.

exclude

OptionDefault
excludenone A CSS selector, a predicate (element) => boolean, or an array of either. Matching elements and their subtrees are left out of the vector output, text and layer names included.

It is the only option. Give it to vector({ exclude }) as the default for every export from that capture, or to a single toVector() or toFigma() call, where it wins for that call.

const shot = await snapdom(el, { plugins: [vector({ exclude: '.no-print' })] })
const svg = await shot.toVector({ exclude: ['.no-print', (node) => node.dataset.private === 'true'] })

SnapDOM's own exclude on the snapdom() call also applies: anything it leaves out of the capture never reaches the vector output.

toVector()

shot.toVector(options?) → Promise<string>

Returns SVG shapes and text without foreignObject: a rectangle becomes a <rect> and a line of text becomes a <text>. Open the file in Illustrator, Inkscape, Penpot, Figma or a browser. Unsupported effects are listed in the diagnostics.

  • The browser breaks the lines and the SVG only draws them. A block that occupies more than one line becomes one <text> per visual line, each at its measured baseline, so no editor re-wraps your paragraph differently.
  • Gradients, borders, radii, shadows, transforms, clips and images become native SVG paint. -webkit-text-stroke is a real stroke, and background-clip: text becomes the text's own fill.
  • textLength is written only when a font binary travels with the file. Pinning a line to a width measured with your font, in a viewer that substituted another one, squeezes the glyphs.
  • Native form controls are reconstructed at Chromium's measured proportions from their kind, state and accent colour, and each one is declared as a reconstruction.

The SVG includes its approximation report in an XML comment beneath the <svg> tag.

For the difference from core toSvg(), see how SVG foreignObject keeps HTML inside the capture.

toFigma()

shot.toFigma(options?) → Promise<void>

Puts the capture on the clipboard in Figma's own paste format. Paste into a Figma file and you get frames, editable text, gradients, shadows, borders and images, with nothing installed. It resolves once the clipboard holds the copy.

button.addEventListener('click', async () => {
  await shot.toFigma()
  toast('Copied. Paste it into Figma.')
})

The Figma handoff example walks through copying a product card and checking its layers and fonts.

  • Call it from a user action, on https or localhost. It uses the async clipboard API, and browsers only let a page the user is interacting with write to the clipboard. Outside a secure context it rejects with a message that says so.
  • letter-spacing survives, which Figma's SVG importer cannot do: measured, a 376.8px line of text arrives at 449px through the SVG route, 50px of that from ignored tracking.
  • Nodes whose paint the paste format cannot spell are drawn as vector artwork and carried as an image, unless they hold text, which stays editable.
  • Effects that read what is underneath a node (mix-blend-mode, backdrop-filter) arrive unblended.

Diagnostics

Every approximation (a conic gradient drawn as wedges, a shadow spread folded into a blur, a filter a destination cannot run) is printed to the console under [snapdom-vector], grouped by code with a count and up to three distinct messages, so four hundred identical warnings come out as one line and a count. The codes are stable and greppable, and toVector() also writes the list into the SVG.

A diagnostic that says something will be lost in the destination (the SVG is exactly right in a browser, and Figma runs only part of it) is a warning about where the file is going, not a degradation of the file.

What reaches the output, and what does not

The engine walks the clone SnapDOM already resolved, which is why membership in the tree is not an argument here. In:

  • Pseudo-elements (::before, ::after, ::first-letter) as real nodes with real boxes, with counters and quotes resolved.
  • Shadow DOM, flattened, so slotted content appears once, at the slot.
  • Icon fonts resolved by ligature, and inline <svg> carried as vector with its element ids namespaced per instance.
  • Images from currentSrc, so srcset resolves to what was actually shown, with object-fit and object-position applied. An SVG source is inlined as vector, not rasterized.
  • Scroll offsets, applied, and overflow clipping as a real clip.
  • <canvas> and <video>, as bitmaps. A tainted canvas is omitted with a reason rather than throwing.
  • In-flight animations, at the values of the frame that was captured.

Out:

  • Anything exclude named, and anything SnapDOM itself excludes.
  • display: none, visibility: hidden, and content clipped away by an overflow ancestor.
  • The value inside a native control. The text in an <input>, the chosen <option> of a <select> and a placeholder are painted by the browser itself and no CSS property describes them. The widget's frame, border and background are drawn.
  • Cross-origin iframes. The frame becomes a placeholder.
  • Closed shadow roots, which are unreadable by anyone.

Limits

Review these limits before handing an export to another editor:

  • Chromium. Everything is measured there and the pixel baselines were recorded there. Other engines are not tested, and we do not claim them.
  • Fonts. The SVG embeds readable webfont files when it can. System and locally installed fonts have no file the page can provide; missing or uncovered faces are diagnosed. Editable text in Figma needs a font available there.
  • A system font keyword is not a font name. Text painted by -apple-system or system-ui cannot tell any destination which face to load.
  • Auto Layout is not inferred. Exported frames keep measured positions; they do not reflow when you resize them.
  • backdrop-filter does not survive. Frosted glass is a blur of whatever sits behind the panel, and neither SVG nor Figma can say that about an arbitrary backdrop. The panel arrives as a flat colour, and the diagnostics name it.
  • A conic gradient becomes 128 flat wedges plus a small disc capping the apex. SVG has no conic paint server.
  • Shadow spread is approximate. feMorphology's kernel is rectangular, so the spread corners are.
  • 3D transforms are flattened to their 2D projection, with a diagnostic.
  • Figma has no colour-matrix effect, so filter: saturate(…) and its relatives have no equivalent there. They are named, with the CSS verbatim, rather than dropped quietly.
  • The capture is one subtree, one viewport, one moment. No page-wide capture, no responsive variants, no component variants inferred from a static picture.

Questions

Is this a tracer? Does it vectorise the pixels?

No. Nothing is traced and nothing is recognised. Every box, line and baseline is read from the live layout, and every fill is parsed from the CSS that produced it. A tracer cannot tell you the second stop of a gradient; this hands you its colour and position.

Will my page come into Figma perfect?

Some CSS effects have no Figma equivalent, including colour-matrix filters, some OpenType settings and per-range baseline shifts. Review the diagnostics and the pasted result.

Do I need a Figma plugin?

No. Call toFigma() from a button, then paste into Figma. Use toVector() when you need an SVG file for another editor.

Does anything leave the machine?

The exporter runs in your page and writes to your clipboard. It does not upload the capture. SnapDOM may fetch page assets; pasting into Figma shares the artwork with that application.

Which browsers?

Chromium today. Everything is measured there and the pixel baselines were recorded there. We have not run it against Firefox or WebKit, so we are not claiming them.

What if it does not work for my page?

Report a minimal example with the browser, export method and diagnostics.

Try it hereLive Vector export

Editable source

Generated output

Nothing generated yet.

Edit the source, then generate a Vector.

Code for this demo