SVG and JavaScript: select, create and change shapes
How to work with SVG in JavaScript: select shapes, change attributes, create elements with createElementNS, handle clicks, measure, and save the result.
Published
An SVG written inline in a web page is part of the page’s DOM. Every <circle>, <path> and <g> is an element that JavaScript can find with querySelector, change with setAttribute, and listen to with addEventListener, exactly as it does with HTML. One thing is different: a new SVG element must be made with document.createElementNS, not document.createElement.
<svg viewBox="0 0 100 100" width="200">
<circle id="dot" cx="50" cy="50" r="30" fill="teal"/>
</svg>
<script>
const dot = document.querySelector('#dot');
dot.addEventListener('click', () => {
dot.setAttribute('fill', 'crimson');
});
</script>
Click the circle and it turns red.
Select and change
Shapes are selected like any other element, by id, class, tag name or attribute:
const dot = document.querySelector('#dot');
const allPaths = document.querySelectorAll('svg path');
const inGroup = document.querySelectorAll('#layer1 > *');
Then change them:
| To do this | Write |
|---|---|
| Set an attribute | dot.setAttribute('r', 40) |
| Read an attribute, as text | dot.getAttribute('r') |
| Read a length as a number | dot.r.baseVal.value |
| Set a CSS property | dot.style.fill = 'crimson' |
| Add or remove a class | dot.classList.add('active') |
Read a data- attribute | dot.dataset.name |
| Hide | dot.setAttribute('visibility', 'hidden'), or dot.style.display = 'none' |
| Move | dot.setAttribute('transform', 'translate(10 0)') |
| Remove | dot.remove() |
Two things catch people out:
stylebeats the attribute.dot.style.fill = 'crimson'writes an inline style, and a style always wins over afill="..."attribute on the same shape. If an attribute seems to be ignored, look for a style or a stylesheet rule setting the same property. Changing SVG colour with CSS explains the order.classNameis not a string. On an SVG elementdot.classNameis an object, sodot.className = 'active'does not work: it is ignored, or in a module or strict-mode script it throws aTypeError. UseclassList, orsetAttribute('class', 'active').
Attribute names keep their capitals in script: setAttribute('viewBox', '0 0 50 50'). setAttribute('viewbox', ...) creates a different attribute that the browser ignores.
Create shapes
SVG elements live in the SVG namespace, and document.createElement('circle') makes an HTML element that happens to be called circle. It is added to the page and draws nothing. Use createElementNS with the namespace as the first argument:
const SVG_NS = 'http://www.w3.org/2000/svg';
function make(tag, attributes) {
const el = document.createElementNS(SVG_NS, tag);
for (const [name, value] of Object.entries(attributes)) {
el.setAttribute(name, value);
}
return el;
}
const svg = document.querySelector('svg');
svg.append(make('rect', { x: 10, y: 10, width: 30, height: 30, fill: 'gold' }));
svg.append(make('circle', { cx: 70, cy: 70, r: 15, fill: 'teal' }));
A gold square and a teal circle are added to the drawing. Attributes take plain setAttribute: only the element needs the namespace. To build a whole SVG this way, create the <svg> element with createElementNS too.
For markup you already have as text, innerHTML is shorter. Set on an SVG element, it parses the text as SVG:
svg.innerHTML += '<line x1="0" y1="0" x2="100" y2="100" stroke="crimson"/>';
Use it only with text you wrote yourself. Markup from users or other sites can carry event-handler attributes that run as script.
Clicks, and where they landed
Events work as in HTML: click, pointerdown, pointermove, pointerenter and the rest, on any shape or on the <svg>. The event reports its position in screen pixels, event.clientX and event.clientY. The drawing uses its own coordinates, set by the viewBox, so a click must be converted before it can be used to place a shape:
<svg viewBox="0 0 100 100" width="300" style="border: 1px solid silver">
</svg>
<script>
const svg = document.querySelector('svg');
function toSvgPoint(event) {
const point = new DOMPoint(event.clientX, event.clientY);
return point.matrixTransform(svg.getScreenCTM().inverse());
}
svg.addEventListener('click', (event) => {
const { x, y } = toSvgPoint(event);
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
dot.setAttribute('cx', x);
dot.setAttribute('cy', y);
dot.setAttribute('r', 3);
svg.append(dot);
});
</script>
Each click adds a dot under the pointer. getScreenCTM() returns the matrix that maps the drawing’s coordinates to the screen, and its inverse maps the screen back to the drawing. It stays correct when the SVG is scaled, scrolled or stretched. SVG viewBox explained covers the coordinate system.
A shape with fill="none" only responds on its stroke. Set pointer-events="all" on it to make the whole interior respond.
Measure
| Call | Returns |
|---|---|
shape.getBBox() | The smallest box round the shape, as x, y, width, height in the drawing’s coordinates. It leaves out the stroke and ignores any transform on the shape |
shape.getBoundingClientRect() | The box on the screen in pixels, after the viewBox scaling and any transforms |
path.getTotalLength() | The length of a path or basic shape, in the drawing’s units |
path.getPointAtLength(d) | The point at distance d along it |
getBBox() on the root <svg> gives the extent of the whole drawing, strokes aside, which is how to work out a viewBox that fits. getTotalLength() is what the line-drawing effect in SVG animation with CSS needs.
Only inline SVG is in the page
What a script can reach depends on how the SVG was placed.
| Placement | From the page’s script |
|---|---|
Inline <svg> | Everything, directly |
<object data="file.svg"> | Through object.contentDocument, once it has loaded, if the file is on the same origin |
<img src="file.svg"> or CSS background | Nothing. The picture is sealed |
For an <object>:
const object = document.querySelector('object');
object.addEventListener('load', () => {
const svgDoc = object.contentDocument;
svgDoc.querySelector('circle').setAttribute('fill', 'crimson');
});
To script a file that is shown as an image, load its text and put it in the page:
async function inlineSvg(url, holder) {
const response = await fetch(url);
holder.innerHTML = await response.text();
}
inlineSvg('/images/map.svg', document.querySelector('#map'));
The SVG is then inline inside the element with the id map. Do this only with files you trust, for the reason given above. SVG in HTML compares the placements.
An SVG file can also carry its own <script> element. It runs when the file is opened directly or shown in an <object> or <iframe>, and never when the file is shown with <img> or as a background.
Save the result
To turn a drawing built in the page back into a file, serialise it:
const svg = document.querySelector('svg');
const text = new XMLSerializer().serializeToString(svg);
const blob = new Blob([text], { type: 'image/svg+xml' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'drawing.svg';
link.click();
XMLSerializer writes the xmlns attribute that a stand-alone file needs. svg.outerHTML leaves it out when the markup did not have it, and the saved file then fails to open as an image: see SVG xmlns. Styles that came from the page’s stylesheet are not part of the element and are not saved with it.
For a PNG of the result, open the saved file in SVG to PNG.
Common problems
| What you see | Cause | Fix |
|---|---|---|
| A new shape is in the DOM but invisible | Made with createElement | createElementNS('http://www.w3.org/2000/svg', tag) |
querySelector returns null | The script ran before the SVG existed, or the SVG is in an <img> | Put the script after the SVG or use defer; inline the SVG |
setAttribute('fill', ...) changes nothing | A style or stylesheet rule sets fill | Set el.style.fill, or remove the rule |
el.className = '...' has no effect, or throws a TypeError | className is an object on SVG elements | el.classList or setAttribute('class', ...) |
| A new viewBox is ignored | Written viewbox | setAttribute('viewBox', ...) |
| Shapes appear in the wrong place after a click | Screen pixels used as drawing coordinates | Convert with getScreenCTM().inverse() |
contentDocument is null | The <object> has not loaded, or the file is on another origin | Wait for load; serve the file from the same origin |
| A saved file will not open | No xmlns | Serialise with XMLSerializer |
| A new shape covers the others | It was appended last, so it is painted last | Insert it earlier: see SVG z-index |
Questions
Do I need a library? No. Everything here is built into the browser. Libraries such as D3 and SVG.js wrap the same calls in shorter ones, which pays off for charts and for long animations.
Does this work in Node.js? Node has no DOM, so document does not exist there. An SVG can still be written as a string, since it is only text, and saved with the file system functions.
How do I animate from script? Change an attribute on each frame inside requestAnimationFrame, or leave the timing to the browser with CSS or SVG’s own animation elements and use script only to start and stop them.
How do I see what a file contains before scripting it? Open it in the SVG viewer, which shows the code and counts the paths, groups and shapes.