Designing and implementing a browser-only SVG editor
Key takeaways from this article
- I teamed up with ChatGPT again—mostly in a project manager role—to build an SVG editor (/en/tools/svgedit/), and this is a log of all the places I stumbled.
- There is no way I could have finished the project alone; generative AI really does shine at coding assistance.
- At the same time, “leaving everything to the AI” did not work. I had to coordinate the overall design, correct misunderstandings, and steer troubleshooting, exactly like a playing manager.
- In that sense, the experience offered a hint about what it takes to work alongside AI in the real world.
Designing and implementing a browser-only SVG editor
What is SVG?
SVG (Scalable Vector Graphics) is an XML-based vector image format standardized by the W3C. Unlike raster images such as PNG or JPEG, SVG graphics can be scaled up and down without any loss of quality. Because an SVG can be embedded directly into an HTML document as part of the DOM, JavaScript and CSS can manipulate and style it dynamically. The specification follows the W3C SVG Recommendation and supports shapes, text, paths, gradients, filters, and much more.
SVG is used in many contexts:
- Displaying icons and diagrams on the web
- Generating dynamic charts and diagrams
- Building drawing applications that react to user input
- Creating assets for print or UI design
Overview of the editor
The editor described here is a fully client-side tool that runs inside the browser. Users can draw not only rectangles and ellipses but also stars, speech balloons, cloud shapes, hearts, and freeform curves with a pen mode. The shapes can be moved, resized, and rotated. The UI also supports alignment, snapping, grouping, and a layer list. Completed artwork can be exported as either SVG or PNG.
The sections below explain how I translated the SVG specification into JavaScript and where I tripped up along the way.
Implementing the specification and the obstacles along the road
Adding shapes and managing attributes
The SVG specification defines dedicated elements and attributes for each type of shape. Rectangles use <rect>
, ellipses use <ellipse>
, polygons use <polygon>
, and each has attributes such as x
, y
, width
, height
, cx
, cy
, rx
, ry
, or points
that must be set correctly. More complex shapes like stars or hearts are created with <path>
elements that combine Bézier curves and arc commands.
Example implementation (adding a rectangle)
function addRectangle(x, y, width, height) {
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("width", width);
rect.setAttribute("height", height);
rect.setAttribute("fill", "transparent");
rect.setAttribute("stroke", "#000");
svg.appendChild(rect);
}
Rectangles and ellipses are straightforward, but polygons and custom shapes require coordinate calculations. To draw a star, for example, divide the central angle evenly and calculate the vertices before writing them to the points
attribute.
function createStar(cx, cy, spikes, outerR, innerR) {
let points = "";
const step = Math.PI / spikes;
for (let i = 0; i < 2 * spikes; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const x = cx + r * Math.cos(i * step);
const y = cy + r * Math.sin(i * step);
points += `${x},${y} `;
}
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
polygon.setAttribute("points", points.trim());
polygon.setAttribute("stroke", "#000");
polygon.setAttribute("fill", "transparent");
svg.appendChild(polygon);
}
Transformations and coordinate systems
SVG allows you to combine translate
, rotate
, and scale
through the transform
attribute. The tricky part was getting the bounding box of the selected shape while taking the local and global coordinate systems into account. The solution was to call getScreenCTM()
to convert everything into the global coordinate system before computing the rectangle.
Applying the coordinate transform
function getTransformedBBox(element) {
const bbox = element.getBBox();
const matrix = element.getScreenCTM();
const points = [
svg.createSVGPoint(), svg.createSVGPoint(),
svg.createSVGPoint(), svg.createSVGPoint()
];
points[0].x = bbox.x; points[0].y = bbox.y;
points[1].x = bbox.x + bbox.width; points[1].y = bbox.y;
points[2].x = bbox.x; points[2].y = bbox.y + bbox.height;
points[3].x = bbox.x + bbox.width; points[3].y = bbox.y + bbox.height;
return points.map(p => p.matrixTransform(matrix));
}
With this in place, the selection frame stays correct even after rotations or scaling.
Snapping and the grid
The grid is rendered with an SVG <pattern>
in the background. Snapping rounds coordinates in JavaScript using a simple helper.
function snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize;
}
For object snapping, the editor scans other shapes for centers and endpoints, finds the closest candidate, and displays guide lines to “magnetize” the selected shape. This dramatically improved the precision of the UI.
Editing text
Because an SVG <text>
element cannot be edited in place, the editor temporarily overlays an HTML <input>
for inline editing.
function editTextElement(textElem) {
const input = document.createElement("input");
input.type = "text";
input.value = textElem.textContent;
document.body.appendChild(input);
const bbox = textElem.getBoundingClientRect();
input.style.position = "absolute";
input.style.left = bbox.x + "px";
input.style.top = bbox.y + "px";
input.focus();
input.addEventListener("blur", () => {
textElem.textContent = input.value;
document.body.removeChild(input);
});
}
Paths and freehand curves
The <path>
element uses commands such as M, L, C, and A. I started with straight-line editing where every click adds another coordinate.
let pathData = "M100,100";
function addPathPoint(x, y) {
pathData += ` L${x},${y}`;
path.setAttribute("d", pathData);
}
Exporting as SVG or PNG
When saving, the editor strips helper elements and then exports. PNG conversion goes through a canvas.
function exportToPNG(svgElement) {
const serializer = new XMLSerializer();
const source = serializer.serializeToString(svgElement);
const img = new Image();
const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
canvas.toBlob(blob => {
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "drawing.png";
a.click();
});
};
img.src = url;
}
Handling scrollable canvases
For large artboards, wrapping the SVG in a <div style="overflow:auto">
and showing scrollbars as needed was enough to keep the UI usable.
Wrapping up
The SVG specification is powerful, but implementing it requires wrestling with coordinate transforms, path definitions, and more. This editor organizes those concerns and delivers the following features:
- Attribute handling for rectangles, ellipses, polygons, and paths
- Proper use of
transform
and coordinate systems - Grid and snapping support
- Inline text editing
- Path editing (line segments for now)
- Export to both PNG and SVG
- Large-canvas support with scrolling
Harnessing the expressiveness of SVG made it possible to build a high-functioning editor that runs entirely in the browser.