Building a Live SVG Editor in the Browser — Syntax Highlighting Without a Library
A live SVG editor sounds simple: textarea on the left, preview on the right, update on change. In practice, there are several sharp edges.
Rendering SVG Safely
The naive approach — innerHTML = userInput — is an XSS vector. SVG can contain <script> tags and event handlers.
The safer approach uses DOMParser:
function renderSVG(svgString, container) {
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, 'image/svg+xml');
const parserError = doc.querySelector('parsererror');
if (parserError) {
return { ok: false, error: parserError.textContent };
}
// Remove script elements
doc.querySelectorAll('script').forEach(el => el.remove());
// Remove event handlers
doc.querySelectorAll('*').forEach(el => {
[...el.attributes].forEach(attr => {
if (attr.name.startsWith('on')) el.removeAttribute(attr.name);
});
});
container.innerHTML = '';
container.appendChild(doc.documentElement.cloneNode(true));
return { ok: true };
}
Enter fullscreen mode Exit fullscreen mode
Export to PNG
SVG → PNG conversion in the browser requires an intermediate step through an <img> element:
async function svgToPng(svgString, width, height) {
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
resolve(canvas.toDataURL('image/png'));
};
img.src = url;
});
}
Enter fullscreen mode Exit fullscreen mode
ViewBox and Zoom
SVG has its own coordinate system defined by viewBox. When you zoom, you’re scaling the container, not changing the viewBox. This keeps the SVG coordinates stable:
function setZoom(containerEl, scale) {
containerEl.style.transform = `scale(${scale})`;
containerEl.style.transformOrigin = 'top left';
}
Enter fullscreen mode Exit fullscreen mode
Snippet Insertion
For quick edits, I added insert buttons for common shapes. The trick is inserting at the cursor position in the textarea:
function insertAtCursor(textarea, snippet) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const before = textarea.value.slice(0, start);
const after = textarea.value.slice(end);
textarea.value = before + snippet + after;
textarea.selectionStart = textarea.selectionEnd = start + snippet.length;
textarea.dispatchEvent(new Event('input'));
}
Enter fullscreen mode Exit fullscreen mode
Live at toolzip.app/tools/svg-preview.