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 toListen for
A click or tapclick
The pointer arriving and leavingmouseenter and mouseleave, or pointerenter and pointerleave
Movement over the shapepointermove
Press and release, for draggingpointerdown, pointermove, pointerup
A right clickcontextmenu, with event.preventDefault() to stop the browser’s menu
A double clickdblclick
The scroll wheelwheel
A key, on a focused shapekeydown

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 circleA click in the middle
fill="teal"Hits the circle
fill="none" with a strokePasses 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:

ValueResponds on
auto, visiblePaintedThe fill if there is one, the stroke if there is one. Visible shapes only
visibleFill, visibleStroke, visibleThe inside, the outline, or both, whether or not they are painted. Visible shapes only
painted, fill, stroke, allThe same four choices, even when the shape has visibility="hidden"
bounding-boxThe 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
noneNothing: 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 it fill="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. mouseover and mouseout bubble, so a listener on a group fires again each time the pointer crosses from one child to another. mouseenter and mouseleave do 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 the data- 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.

PlacementEvents 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 backgroundNo. 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 seeCauseFix
Clicks only register on the outlinefill="none"pointer-events="all" or fill="transparent"
Nothing respondsThe SVG is in an <img> or a backgroundInline the SVG
The wrong shape gets the clickAnother shape lies on top, possibly invisiblepointer-events="none" on the one on top
An invisible shape blocks clicksopacity="0" still catches eventspointer-events="none", or visibility="hidden"
The handler fires many times on hovermouseover bubbling from childrenmouseenter
event.target is a <path>, not the regionThe hit shape is a child of the groupevent.target.closest('g')
shape.click is not a functionSVG elements have no click()dispatchEvent(new MouseEvent('click', { bubbles: true }))
Dragging scrolls the page on a phoneThe browser handles the touch firsttouch-action: none on the SVG
parent.fn is not a function from an objectThe function is not a global of the pageDeclare 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.