Convert DOM elements to clean, self-contained SVG files.
Built for exporting node-based editors (SvelteFlow, React Flow, etc.) to vector graphics. Handles mixed HTML/SVG structures that existing libraries fail on.
A node-based pipeline editor — DOM on the left, exported SVG on the right:
Real-world UI components (stat cards, avatars, alerts, buttons, progress bars, charts, palettes, tables, toasts, tags, toggles, breadcrumbs, donut charts, skeleton loaders, keyboard shortcuts):
55+ CSS features rendered as DOM elements and their SVG counterparts:
npm install github:milanofthe/dom2svgCapture any web page as a vector SVG from the command line. Uses headless Chrome via Puppeteer to load the page, then runs dom2svg against it.
Requires Chrome/Chromium installed locally. Puppeteer-core is included as a dev dependency.
# Full page
npx dom2svg https://example.com -o page.svg
# Specific element with background and padding
npx dom2svg https://news.ycombinator.com -s "#hnmain" -b white -p 12 -o hn.svg
# Pipe to stdout
npx dom2svg https://example.com > page.svg
# Wait for JS-rendered content
npx dom2svg https://app.example.com --wait 3000 -o app.svg
# Custom viewport size
npx dom2svg https://example.com --width 1920 --height 1080 -o wide.svgOptions:
| Flag | Description | Default |
|---|---|---|
-o, --output <file> |
Output file (default: stdout) | stdout |
-s, --selector <css> |
CSS selector for target element | body |
-b, --background <color> |
SVG background color | transparent |
-p, --padding <px> |
Padding around the element | 0 |
--width <px> |
Viewport width | 1280 |
--height <px> |
Viewport height | 800 |
--wait <ms> |
Extra wait after page load | 0 |
--chrome <path> |
Chrome executable path | auto-detect |
Chrome is auto-detected on Windows, macOS, and Linux. Override with --chrome or the CHROME_PATH environment variable.
import { domToSvg } from "dom2svg";
const element = document.querySelector("#my-editor");
const result = await domToSvg(element);
// Download as .svg file
result.download("export.svg");const result = await domToSvg(element, {
background: "#ffffff",
padding: 20,
});const result = await domToSvg(element);
// Trigger browser download
result.download("diagram.svg");
// Get SVG markup (e.g. for saving to a server)
const svgString = result.toString();
// Get a Blob (e.g. for FormData upload)
const blob = result.toBlob();// By CSS selector
const result = await domToSvg(element, {
exclude: ".toolbar, .minimap, [data-no-export]",
});
// By predicate
const result = await domToSvg(element, {
exclude: (el) => el.tagName === "BUTTON",
});Convert text to <path> elements so the SVG renders identically without any fonts installed. Requires opentype.js (bundled dependency).
const result = await domToSvg(element, {
textToPath: true,
fonts: {
// Simple: family → URL
"Inter": "/fonts/Inter-Regular.woff2",
// Detailed: family → config with weight/style
"Inter": {
url: "/fonts/Inter-Bold.woff2",
weight: "700",
style: "normal",
},
},
});Override rendering for specific elements:
const result = await domToSvg(element, {
handler: (el, ctx) => {
// Replace a placeholder with a custom SVG shape
if (el.classList.contains("chart-placeholder")) {
const circle = ctx.svgDocument.createElementNS(
"http://www.w3.org/2000/svg",
"circle",
);
circle.setAttribute("cx", "50");
circle.setAttribute("cy", "50");
circle.setAttribute("r", "40");
circle.setAttribute("fill", "#3b82f6");
return circle;
}
return null; // fall through to default rendering
},
});<script>
import { domToSvg } from "dom2svg";
async function exportDiagram() {
const editor = document.querySelector(".svelte-flow");
const result = await domToSvg(editor, {
background: "#ffffff",
padding: 24,
exclude: ".svelte-flow__controls, .svelte-flow__minimap",
});
result.download("diagram.svg");
}
</script>
<button onclick={exportDiagram}>Export SVG</button>import { domToSvg } from "dom2svg";
function ExportButton() {
const handleExport = async () => {
const editor = document.querySelector(".react-flow");
const result = await domToSvg(editor, {
background: "#ffffff",
padding: 24,
exclude: ".react-flow__controls, .react-flow__minimap",
});
result.download("flowchart.svg");
};
return <button onClick={handleExport}>Export SVG</button>;
}Converts a DOM element tree into a self-contained SVG.
Parameters:
| Parameter | Type | Description |
|---|---|---|
element |
Element |
The root DOM element to convert |
options |
DomToSvgOptions |
Optional configuration (see below) |
Returns: Promise<DomToSvgResult>
| Property | Type | Description |
|---|---|---|
svg |
SVGSVGElement |
The generated SVG element |
toString() |
string |
Serialized SVG with XML declaration |
toBlob() |
Blob |
SVG as image/svg+xml blob |
download(filename?) |
void |
Triggers browser download |
interface DomToSvgOptions {
/** Background color for the SVG (default: transparent) */
background?: string;
/** Padding around the captured area in px (default: 0) */
padding?: number;
/** CSS selector or predicate to exclude elements */
exclude?: string | ((element: Element) => boolean);
/** Convert text to <path> elements using opentype.js (default: false) */
textToPath?: boolean;
/** Font mapping for text-to-path (family name → URL or config) */
fonts?: Record<string, string | { url: string; weight?: string; style?: string }>;
/** Custom element handler — return SVGElement to override, null for default */
handler?: (element: Element, context: RenderContext) => SVGElement | null;
}| Feature | Support |
|---|---|
| Backgrounds | |
| Background colors | Full |
| Linear gradients | Full (correct diagonal angles on non-square elements) |
| Radial gradients | Full (circle and ellipse, rasterized via Canvas) |
| Conic gradients | Full (rasterized via Canvas) |
| Multiple backgrounds | Full (layered in correct CSS order) |
| Background size/position | Full (contain, cover, explicit sizes) |
Background images (url()) |
Full (inlined as data URLs) |
| Borders & Outlines | |
| Borders (uniform and per-side) | Full (solid, dashed, dotted) |
| Border radius (uniform and non-uniform) | Full (including pill shapes) |
| Outline | Full (solid, dashed, dotted with offset) |
| Shadows | |
| Box shadow | Full (outer and inset, multiple, spread, blur) |
| Text shadow | Full (single and multiple, via SVG filters) |
| CSS Filters | |
blur() |
Full (via feGaussianBlur) |
brightness() |
Full (via feComponentTransfer) |
contrast() |
Full (via feComponentTransfer) |
drop-shadow() |
Full (via feDropShadow) |
grayscale() |
Full (via feColorMatrix) |
hue-rotate() |
Full (via feColorMatrix) |
invert() |
Full (via feComponentTransfer) |
opacity() |
Full (via feComponentTransfer) |
saturate() |
Full (via feColorMatrix) |
sepia() |
Full (via feColorMatrix) |
| Filter chaining | Full (multiple filters compose in order) |
| Layout & Clipping | |
| CSS transforms | Full (translate, rotate, scale, skew, matrix) |
| Transform origin | Full |
| Opacity | Full |
| Overflow clipping | Full (hidden, clip, scroll, auto) |
clip-path |
Full (inset, circle, ellipse, polygon, path) |
| Z-index / stacking contexts | Full (CSS 2.2 paint order) |
| Elements | |
| Inline SVGs | Full (deep clone with ID namespacing) |
<img> elements |
Full (inlined as data URLs, border-radius clipping) |
<canvas> elements |
Full (via toDataURL()) |
| Form elements | Full (<input>, <select>, <textarea> with multiline) |
Pseudo-elements (::before, ::after) |
Full (text content, browser-measured positioning) |
| List markers | Full (disc, circle, square, decimal) |
| Text | |
| Text rendering | Full (<text> elements, or <path> with textToPath) |
| Text decoration | Full (underline, line-through) |
| Text transform | Full (uppercase, lowercase, capitalize) |
| Text overflow (ellipsis) | Full (appends ellipsis character) |
| Letter spacing | Full |
| Visibility | |
visibility: hidden |
Correctly skipped (children still rendered) |
display: none |
Correctly skipped |
opacity: 0 |
Rendered as group with opacity="0" (subtree preserved) |
src/
├── index.ts # domToSvg() entry point
├── types.ts # All TypeScript interfaces
├── core/
│ ├── traversal.ts # DOM tree walking
│ └── styles.ts # CSS property parsing
├── renderers/
│ ├── html-element.ts # HTML → SVG (backgrounds, borders, overflow, pseudo)
│ ├── svg-element.ts # SVG cloning with ID namespacing
│ └── text-node.ts # Text → <text> or <path>
├── assets/
│ ├── images.ts # Image/canvas → data URL inlining
│ ├── fonts.ts # Font loading + text-to-path (opentype.js)
│ ├── gradients.ts # CSS gradient → SVG gradient / rasterized image
│ ├── filters.ts # CSS filters → SVG filters (blur, grayscale, sepia, etc.)
│ ├── box-shadow.ts # box-shadow → SVG filter / mask
│ ├── clip-path.ts # clip-path → SVG clipPath
│ └── text-shadow.ts # text-shadow → SVG filter
├── transforms/
│ ├── parse.ts # CSS transform string parsing
│ ├── matrix.ts # 2D affine matrix operations
│ └── svg.ts # CSS transform → SVG transform attribute
└── utils/
├── dom.ts # SVG namespace helpers, DOM type guards
├── id-generator.ts # Unique ID generation
└── geometry.ts # Bounding box and rounded-rect path utilities
Single runtime dependency: opentype.js (only used when textToPath is enabled).
The CLI additionally uses puppeteer-core for headless Chrome automation.
npm install
npm run build # ESM + CJS + CLI bundles via tsup
npm run type-check # TypeScript strict mode
npm run test # 211 unit tests via Vitest
npm run test:watch # Watch mode
npm run demo # Visual demo at localhost:5173MIT


