SVG events: onclick, hover and pointer-events explained
How click, hover, touch and keyboard events work on SVG shapes: onclick and addEventListener, which part of a shape responds, pointer-events, and dragging.
Published
Every shape in an inline SVG takes the same events as an HTML element: click, mouseover, pointerdown, keydown and the rest. Attach a handler with an onclick attribute or with addEventListener. What differs from HTML is which pixels count as the shape: by default only its painted parts respond.
<svg viewBox="0 0 100 100" width="200">
<circle id="dot" cx="50" cy="50" r="40" fill="teal" style="cursor: pointer"/>
</svg>
<script>
const dot = document.querySelector('#dot');
dot.addEventListener('click', (event) => {
event.target.setAttribute('fill', 'crimson');
});
</script>
Click the circle and it turns red. Click the corner of the SVG, outside the circle, and nothing happens: the circle’s handler does not run.
Two ways to attach a handler
An attribute on the shape:
<script>
function pick(shape) {
shape.setAttribute('fill', 'crimson');
}
</script>
<svg viewBox="0 0 100 100" width="200">
<circle cx="50" cy="50" r="40" fill="teal" onclick="pick(this)"/>
</svg>
Inside the attribute, this is the shape and event is the event object. The function can live anywhere the page can see it, including an external .js file loaded with a <script src> tag: an inline SVG is part of the page, so there is nothing special to do. SVG accepts the same event attributes as HTML, among them onclick, onmouseover, onmouseout, onmousedown, onmousemove and onkeydown.
Or a listener added from script, as in the first example. This keeps the markup clean and lets one listener serve many shapes.
Events by task
| To react to | Listen for |
|---|---|
| A click or tap | click |
| The pointer arriving and leaving | mouseenter and mouseleave, or pointerenter and pointerleave |
| Movement over the shape | pointermove |
| Press and release, for dragging | pointerdown, pointermove, pointerup |
| A right click | contextmenu, with event.preventDefault() to stop the browser’s menu |
| A double click | dblclick |
| The scroll wheel | wheel |
| A key, on a focused shape | keydown |
The pointer events cover mouse, touch and pen with one set of handlers, and event.pointerType says which it was. For a change of colour on hover, no script is needed: use :hover in CSS.
Which part of a shape responds
The pointer-events property decides. Its default, auto, means the same as visiblePainted: the shape responds where it is painted, as long as it is visible. Each row below was tested in Chrome by clicking the middle of a circle.
| The circle | A click in the middle |
|---|---|
fill="teal" | Hits the circle |
fill="none" with a stroke | Passes through. Only the stroke line responds |
fill="transparent" | Hits the circle |
fill="none" pointer-events="all" | Hits the circle |
opacity="0" or fill-opacity="0" | Hits the circle |
visibility="hidden" | Passes through |
display="none" | Passes through |
pointer-events="none" | Passes through |
Two rows surprise people. A shape with fill="none" is hollow to the pointer, so an outlined region on a map only reacts along its border. And a fully transparent shape still catches clicks: the SVG 2 specification states that opacity does not affect event processing.
The values of pointer-events:
| Value | Responds on |
|---|---|
auto, visiblePainted | The fill if there is one, the stroke if there is one. Visible shapes only |
visibleFill, visibleStroke, visible | The inside, the outline, or both, whether or not they are painted. Visible shapes only |
painted, fill, stroke, all | The same four choices, even when the shape has visibility="hidden" |
bounding-box | The whole rectangle round the shape. A newer value: it worked in Chrome, and MDN’s compatibility data lists it for Chrome and Safari but not Firefox |
none | Nothing: the pointer goes through to whatever lies beneath |
It can be written as an attribute or in CSS, and it is inherited, so pointer-events="none" on a <g> switches off everything inside.
Three uses come up again and again:
- Make a hollow shape clickable inside:
pointer-events="all", or give itfill="transparent". - Let clicks through a label or an overlay:
pointer-events="none"on the thing on top. - Make a thin line easy to hit: draw the line twice. Put a wide copy on top with
stroke="transparent"and give that one the handler. In the test, a line one pixel wide on screen was missed by a click two pixels from its centre, and the wide transparent copy caught a click eight pixels away.
Where shapes overlap, the one painted last is on top and receives the event. SVG z-index covers the order.
One listener for many shapes
Events bubble up from the shape through its groups to the <svg>. A single listener on a parent can therefore handle every shape inside it:
<svg viewBox="0 0 100 100" width="200">
<g id="map">
<circle data-name="North" cx="30" cy="50" r="15"/>
<rect data-name="South" x="60" y="35" width="30" height="30"/>
</g>
</svg>
<p id="out"></p>
<script>
document.querySelector('#map').addEventListener('click', (event) => {
document.querySelector('#out').textContent = event.target.dataset.name;
});
</script>
Click either shape and its name appears below. event.target is the shape that was hit and event.currentTarget is the group the listener is on. When regions are groups of several paths, event.target.closest('g') finds the region from whichever path was hit.
Two details:
- Hover on a group.
mouseoverandmouseoutbubble, so a listener on a group fires again each time the pointer crosses from one child to another.mouseenterandmouseleavedo not bubble and fire once for the group. - Shapes drawn with
<use>. The target of a click on a copy is the<use>element, not the shape inside it. Put the id or thedata-attribute on the<use>.
A click on an empty part of the drawing has the <svg> element as its target.
Where the click landed
event.clientX and event.clientY are in screen pixels. To get the point in the drawing’s own coordinates, convert with the SVG’s screen matrix:
function toSvgPoint(svg, clientX, clientY) {
return new DOMPoint(clientX, clientY).matrixTransform(svg.getScreenCTM().inverse());
}
For a mouse or pointer event, pass event.clientX and event.clientY. A touchstart or touchmove event has no clientX of its own: the position is on event.touches[0]. In a test, a tap at 200,100 on a 400 pixel SVG with a 100 unit viewBox gave 50,25 from both a pointerdown handler and a touchstart handler. SVG and JavaScript explains the matrix.
Dragging a shape
<svg viewBox="0 0 100 100" width="400" height="400" style="touch-action: none">
<circle id="knob" cx="20" cy="20" r="8" fill="teal"/>
</svg>
<script>
const svg = document.querySelector('svg');
const knob = document.querySelector('#knob');
function toSvgPoint(event) {
const point = new DOMPoint(event.clientX, event.clientY);
return point.matrixTransform(svg.getScreenCTM().inverse());
}
knob.addEventListener('pointerdown', (event) => {
knob.setPointerCapture(event.pointerId);
});
knob.addEventListener('pointermove', (event) => {
if (!knob.hasPointerCapture(event.pointerId)) return;
const { x, y } = toSvgPoint(event);
knob.setAttribute('cx', x);
knob.setAttribute('cy', y);
});
</script>
Press the circle and it follows the pointer until released. setPointerCapture keeps the moves coming to the circle even when the pointer runs ahead of it or leaves the SVG, and the capture ends by itself on release. touch-action: none stops a touch screen from scrolling the page instead.
Keyboard
Shapes cannot be reached with the Tab key unless told to. A shape that acts as a button needs three things: tabindex="0" to take focus, a role and name for screen readers, and a key handler, since Enter and Space do not produce a click on a shape.
<svg viewBox="0 0 100 100" width="200">
<circle id="zoom" cx="50" cy="50" r="15" tabindex="0" role="button" aria-label="Zoom in"/>
</svg>
<script>
const zoom = document.querySelector('#zoom');
function act() {
zoom.setAttribute('r', Number(zoom.getAttribute('r')) + 5);
}
zoom.addEventListener('click', act);
zoom.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
act();
}
});
</script>
The circle grows on a click, and on Enter or Space when it has focus. Where the thing clicked is really a button, an HTML <button> with the SVG inside it does all of this without script. Accessible SVG covers names and roles.
Triggering a click from script
SVG elements have no click() method: typeof circle.click is undefined in Chrome, where an HTML button has one. Send the event instead:
shape.dispatchEvent(new MouseEvent('click', { bubbles: true }));
Listeners on the shape and on its ancestors then run as for a real click.
SVG files in img and object
How the SVG is placed decides what can listen.
| Placement | Events on shapes |
|---|---|
Inline <svg> | Yes, from the page’s scripts |
<object data="file.svg"> | Yes, but in the file’s own document |
<img src="file.svg"> or a CSS background | No. Only the <img> as a whole can be clicked |
An SVG file in an <object> is a separate document. An onclick inside it cannot see the page’s functions by name. Three things work, all tested in Chrome with the file on the same origin as the page:
- Load the script from the file itself, with
<script href="app.js"/>inside the<svg>. - Call up to the page:
onclick="parent.picked('north')". - Reach in from the page, once the object has loaded:
object.contentDocument.querySelector('circle').addEventListener('click', ...).
In an <img>, scripts in the file do not run and no request was made for the file’s external script. To make the shapes of such a file interactive, put the SVG inline in the page.
Common problems
| What you see | Cause | Fix |
|---|---|---|
| Clicks only register on the outline | fill="none" | pointer-events="all" or fill="transparent" |
| Nothing responds | The SVG is in an <img> or a background | Inline the SVG |
| The wrong shape gets the click | Another shape lies on top, possibly invisible | pointer-events="none" on the one on top |
| An invisible shape blocks clicks | opacity="0" still catches events | pointer-events="none", or visibility="hidden" |
| The handler fires many times on hover | mouseover bubbling from children | mouseenter |
event.target is a <path>, not the region | The hit shape is a child of the group | event.target.closest('g') |
shape.click is not a function | SVG elements have no click() | dispatchEvent(new MouseEvent('click', { bubbles: true })) |
| Dragging scrolls the page on a phone | The browser handles the touch first | touch-action: none on the SVG |
parent.fn is not a function from an object | The function is not a global of the page | Declare it with function at the top level of a page script |
Questions
Can a shape be a link? Yes: wrap it in an SVG <a href="..."> element, which makes it a real link that works without script. SVG links shows how.
Do events work in a standalone SVG file? Yes, when it is opened directly in a browser or shown in an <object>, with the script inside the file. Not when it is shown as an image.
How do I show a label on hover? See SVG tooltips, which covers the <title> element, CSS and JavaScript ways.
How do I find which shape is which? Open the file in the SVG viewer and read its code: the ids and classes the editor wrote are what querySelector needs.