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.
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.
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
| Option | Default | |
|---|---|---|
| exclude | none | 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-strokeis a real stroke, andbackground-clip: textbecomes the text's own fill. textLengthis 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-spacingsurvives, 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, sosrcsetresolves to what was actually shown, withobject-fitandobject-positionapplied. 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
excludenamed, 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-systemorsystem-uicannot 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-filterdoes 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
spreadis 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.