Draggable SVG elements


April 3, 2018 Code on Github

Introduction

[This is a completely rewritten version of a post from 1st September 2011]

One of the most common forms of interaction on a computer is clicking and dragging. I use it a lot for interactive demos, such as those in my SVG tutorial, and have a built a library for making simple draggable SVG diagrams.

You can find the code for all the examples on this page here. This article builds up the code required step-by-step, explaining why each element is needed. If you only care about finished code, you can find it here.

This is the SVG I'm going to build on this page. It's relatively simple, but it demonstrates the code works with different element types. Some of the elements have transformations applied to them. For example, the star is rotated. There is also a static rect element which can't be dragged.

Drag

SVG setup

Let's start with a simple SVG with two rect element, one we want to be draggable and another we don't.

<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 20">
  <rect x="0" y="0" width="30" height="20" fill="#fafafa"/>
  <rect x="4" y="5" width="8" height="10" fill="#007bff"/>
  <rect x="18" y="5" width="8" height="10"   fill="#888"/>
</svg>

CSS

First give the rect we want to make draggable the class "draggable". We can give the other rect the class static. We can then give the user an idea about what they can interactive with by changing the cursor when they mouseover each element

.static {
  cursor: not-allowed;
}
.draggable {
  cursor: move;
}

Javascript outline

The click-and-drag interaction has two obvious parts: click and drag, but really there are three:

  • Pressing the mouse, when we need to find what element, if any, we have pressed on.
  • Dragging the mouse, when we need to move the element.
  • Releasing the mouse, when we need to release the element so it doesn't continue to move when we move the mouse.

This is fairly straightforward but there a bit of subtlety required to get it right.

Let's start by making a makeDraggable function. This can be in a separate Javascript file or in a script element inside the SVG itself. We call this function when the to the SVG element loads, passing in the load event:

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 30 20"
     onload="makeDraggable(evt)">

This function itself gets the SVG element as the event target and binds event listeners to mousedown, mousemove, mouseup, and mouseleave events on the SVG element.

function makeDraggable(evt) {
  var svg = evt.target;
  svg.addEventListener('mousedown', startDrag);
  svg.addEventListener('mousemove', drag);
  svg.addEventListener('mouseup', endDrag);
  svg.addEventListener('mouseleave', endDrag);

  function startDrag(evt) {
  }

  function drag(evt) {
  }

  function endDrag(evt) {
  }
}

Selecting an element

Let's test that we can select and deselect an element. First create a variable selectedElement and set it to null (or something falsy). We add it outside of the dragging function so they can all refer to it, but inside makeDraggable so it's not global.

var selectedElement = false;

Our startDrag function should test whether the target of the mousedown event it's passed has the class draggable, and if so set selectedElement to that element.

function startDrag(evt) {
  if (evt.target.classList.contains('draggable')) {
    selectedElement = evt.target;
  }
}

For now, we'll just make the drag function increment the x attribute of the selected element. Note that we use getAttributeNS and setAttributeNS when use SVG elements. We also have to make sure we convert the attribute into a float before we add 0.1 to it.

I've also add evt.preventDefault(); which blocks any other dragging behaviour. For example, in the SVG at the top of the page, if you drag an element over the text element, it won't highlight the text.

function drag(evt) {
  if (selectedElement) {
    evt.preventDefault();
    var x = parseFloat(selectedElement.getAttributeNS(null, "x"));
    selectedElement.setAttributeNS(null, "x", x + 0.1);
  }
}

The endDrag function just needs to set selectedElement back to null, so we don't keep moving it once the mouse has been released.

function endDrag(evt) {
  selectedElement = null;
}

Now we can click on the draggable (blue) rect, and when we move the mouse, it will slide right. Clicking the static rect and dragging the mouse has no effect.

Dragging an element

Now let's try to actually move the rect with the mouse. We can get the position of the mouse using evt.clientX and evt.clientY, and use these to update the coordinates of the element.

function drag(evt) {
  if (selectedElement) {
    evt.preventDefault();
    var dragX = evt.clientX;
    var dragY = evt.clientY;
    selectedElement.setAttributeNS(null, "x", dragX);
    selectedElement.setAttributeNS(null, "y", dragY);
  }
}

Now if you click and drag the rect, you'll find that it... disappears, or goes somewhere unexpected.

Fixing the coordinates

The problem is that clientX and clientY give the mouse coordinates using the screen coordinate system (which will be something like 300 x 200 pixels, though it depends on how you're viewing the page, since the images are responsive). We need to know the coordinates in SVG space, which is defined by the viewBox attribute, in this case 30 x 20.

To find out how to convert from the screen coordinate system to the SVG coordinate system, we can use the getScreenCTM method of the svg element. This returns the Current Transformation Matrix for the screen, which is an object with six keys, a, b, c, d, e, f.

It's not too important what these values represent; in most cases all we need to know is that if an element has attributes of $(x, y)$, then it will have coordinates on screen of $(ax + e, dy + f)$. So to find the position of the mouse in SVG space, we just calculate the inverse. So let's add a function to do that:

function getMousePosition(evt) {
  var CTM = svg.getScreenCTM();
  return {
    x: (evt.clientX - CTM.e) / CTM.a,
    y: (evt.clientY - CTM.f) / CTM.d
  };
}

Then the drag function becomes:

function drag(evt) {
  if (selectedElement) {
    evt.preventDefault();
    var coord = getMousePosition(evt);
    selectedElement.setAttributeNS(null, "x", coord.x);
    selectedElement.setAttributeNS(null, "y", coord.y);
  }
}

Now when you drag, the rect element moves with your mouse.

Unfortunately, it positions the corner of the rect where your mouse is, even if you select the center of the rect.

Fixing dragging

There are a couple of ways we could fix the dragging issue. I think the easiest is to calculate the offset from where the mouse is first clicked at the top left of the rect. Then, when we set the coordinates of the rect, we can subtract that value.

So calculate the offset in the startDrag function:

var selectedElement, offset;

function startDrag(evt) {
  if (evt.target.classList.contains('draggable')) {
    selectedElement = evt.target;
    offset = getMousePosition(evt);
    offset.x -= parseFloat(selectedElement.getAttributeNS(null, "x"));
    offset.y -= parseFloat(selectedElement.getAttributeNS(null, "y"));
  }
}

And then remove the offset in the drag function.

function drag(evt) {
  if (selectedElement) {
    evt.preventDefault();
    var coord = getMousePosition(evt);
    selectedElement.setAttributeNS(null, "x", coord.x - offset.x);
    selectedElement.setAttributeNS(null, "y", coord.y - offset.y);
  }
}

Now everything should work perfectly.

Until...

Dragging other elements

As we've written it, the drag function works by updating the x and y attributes of the selected element. Unfortunately, most elements don't have x and y attributes. In order to make the function universal, we need to use transforms.

Working with transforms is a little tricky. I did write a version that uses regex to parse the transform attribute for elements, which is a bit hacky, but might be easier to understand. It also might not work if the target element has some convoluted set of transformations already applied.

The version here is better, but might not work on all browsers. The idea is to calculate the offset from the first transform on the element, which should be a translate transform. If the first transform is not a translation, or the element doesn't have a transform, then we add one.

var selectedElement, offset, transform;

function startDrag(evt) {
  if (evt.target.classList.contains('draggable')) {
    selectedElement = evt.target;
    offset = getMousePosition(evt);

    // Get all the transforms currently on this element
    var transforms = selectedElement.transform.baseVal;

    // Ensure the first transform is a translate transform
    if (transforms.length === 0 ||
        transforms.getItem(0).type !== SVGTransform.SVG_TRANSFORM_TRANSLATE) {
      // Create an transform that translates by (0, 0)
      var translate = svg.createSVGTransform();
      translate.setTranslate(0, 0);

      // Add the translation to the front of the transforms list
      selectedElement.transform.baseVal.insertItemBefore(translate, 0);
    }

    // Get initial translation amount
    transform = transforms.getItem(0);
    offset.x -= transform.matrix.e;
    offset.y -= transform.matrix.f;
  }
}

The drag function then updates the translation transform to the mouse position minus the offset:

function drag(evt) {
  if (selectedElement) {
    evt.preventDefault();
    var coord = getMousePosition(evt);
    transform.setTranslate(coord.x - offset.x, coord.y - offset.y);
  }
}

Now we have a makeDraggable function that should work on any element regardless of its type and the transformations applied to it.

The order of the elements stays the same, so the ellipse will always be in front of the rect elements, but behind the others. I'll write a separate tutorial on how to move elements up and down in the z-axis.

Drag

Working on mobile

On final thing: getting mouse events to work on mobile and touch devices. The first thing we need to do is to add handlers for the touch events:

svg.addEventListener('touchstart', startDrag);
svg.addEventListener('touchmove', drag);
svg.addEventListener('touchend', endDrag);
svg.addEventListener('touchleave', endDrag);
svg.addEventListener('touchcancel', endDrag);

Then, because there can be multiple touches, we need to use only the position of the first one. So we have to update the getMousePosition function:

function getMousePosition(evt) {
  var CTM = svg.getScreenCTM();
  if (evt.touches) { evt = evt.touches[0]; }
  return {
    x: (evt.clientX - CTM.e) / CTM.a,
    y: (evt.clientY - CTM.f) / CTM.d
  };
}

Working with groups

There are still a few elements this code won't work with: groups and foreignObjects. The reason is that these elements wrap child elements.

In the case of foreignObjects you can add a style to prevent the child element from responding to mouse events. Otherwise the code will attempt to add transform attributes to them, which won't work.

foreignObject * {
  pointer-events: none;
}

With groups, the groups themselves to do not capture mouse events, so we need to get the group element from the child element.

selectedElement = evt.target.parentNode;

You can find the full code here, which allows for dragging both individual elements and groups (e.g. the star and ellipse).

Drag

Comments (36)

Michael Everitt on Feb. 23, 2012, 5:22 p.m.

It must be challenging to step back and write a description for beginners.

Your write-up for dragging svg shapes is just about perfect for me.

The step-by step breakdown makes the parts digestible, the attention to formatting and naming contributes a lot to understandability, the isolated example given at the end helps to firm up a holistic view, and the way you touch on topics that could be hugely distracting, such as the transformation matrix usage, with just enough information to lead to success, is really admirable.

Thank you for a great description.

Peter on Feb. 23, 2012, 11:42 p.m.

Thanks for your kind words Michael, I appreciate you taking the time to write. It's very encouraging to hear that this has been of use and makes me want get on with writing a full SVG tutorial. I'm not that far beyond a beginner, so it's not much of a step to write something I think I would have found useful.

All the best, Peter

zass on April 11, 2012, 8:44 p.m.

Thanks for this great post! Any way to get this to work with a group <g> of elements? I have more complex shapes made through <g> that I want to move together as a unit.

Peter on April 11, 2012, 9:05 p.m.

Thanks. I've answered a similar question on StackOverflow (http://stackoverflow.com/questions/7777010/svg-dragging-for-group/7778990#7778990). If you look at the code for the example you should be able to work it out. The only real difference is to select the parent element when you click on a shape. One day I'll get around to writing an proper tutorial here.

mO on June 20, 2012, 3:05 p.m.

I'd like to second Michael Everitt's sentiments precisely. Thanks-a-bunch for this well delivered info.

One question .. what would I need to tweak to allow the selected shape to only be able to move along the x OR y axis.

Well ... I'm really most interested in the X axis ... I include the Y axis out of sheer curiosity :)

mO on June 20, 2012, 3:37 p.m.

Nevermind about X or Y axis only movement. I found a simple solution.

Changed "currentMatrix[5] += dy" to "currentMatrix[5] = dy" (+= to =). Seems to work nicely ... let me know if there are reasons to do it differently :)

Next question though, is about dragging <def>'d shapes. It seems to only work if I explicitly call out a <rect> ... the dragging doesn't seem to work on shapes drawn via <use> ... thoughts?

Nuno Goncalves on March 28, 2013, 2:02 p.m.

Your attachement drag_elements.svg have a small bug.

function deselectElement(evt) {
if(selectedElement != 0){
selectedElement = 0;
selectedElement.removeAttributeNS(null, "onmousemove");
selectedElement.removeAttributeNS(null, "onmouseout");
selectedElement.removeAttributeNS(null, "onmouseup");
}
}

Note that you are clearing selectedElement before using it to remove the handlers.

Peter on April 14, 2013, 11:16 p.m.

Thanks for pointing out the mistake Nuno. I must have forgotten to update the code. I've updated it now and it should work properly.

Aslam on July 4, 2013, 8:49 p.m.

Very very thanks to you.. I dont know how to say thanks to you.. Because I was looking for svg code with moving elements. I saw many site with moving elements in svg. But those were working with me. But this code is working well with me..

But if we drag fastly,, it causes mose control ... why..

by Aslam.Pk

Phil on Oct. 22, 2013, 7:22 p.m.

Thanks for the excellent introduction! I had one issue: the script stops responding if you move the mouse fast enough to get the cursor outside the bounds of the element before the code in moveElement finishes executing.

I was able to resolve this problem by simply replacing the line:

selectedElement.setAttributeNS(null, "onmousemove", "moveElement(evt)");

with:

document.onmousemove = moveElement;

and replacing the line:

selectedElement.removeAttributeNS(null, "onmousemove");

with:

document.onmousemove = null;

This way the script pays attention to all mouse move events in the document, even if the mouse isn't over the element for brief periods as things are moving around.

I found this solution here.

Michael Albert on Dec. 15, 2013, 10:41 p.m.

Thank you very much for producing such an extremely useful document!There are two small issues which I wonder if you've thought about. 1) If, while dragging and continuing to hold down the button on the mouse, the cursor leaves the window even briefly and then re-enters, the object is no longer being dragged. I used to do a little programming under the X-Windows system, and it had two features (i) while the mouse button was held down, events were delivered to the original window and (ii) one could determine the state of the mouse buttons in the context of a mouse movement event. It seems HTML5/javascript/SVG doesn't work that way.2) In Firefox 25.0.1, about a quarter of the time, instead of dragging the object as expected, I find myself dragging a translucent copy of one or both squares. It is as though some other "drag-and-drop" mechnism is competing with the one you have implemented. Have you noticed this? Again, thank you and best wishes, Mike Albert

Dave on Feb. 16, 2014, 6:52 a.m.

I noticed the same thing as Michael Albert. Working on trial and error to attack. I'm going to look into what Phil said too...sounds possibly germane. Else, what a great doc, it wraps up a number of points in a succinct useful form. Like the writer above mentions, I am trying to ignore Y, and also to limit the range of dragging.

Anonymous on Feb. 18, 2014, 12:29 p.m.

Unfortunately there is a glaring flaw in this. this works because you have created an SVG 400x200 with a viewbox also 400x200, a pixel as returned by clientX etc is the same size as a pixel in the transformation matrix. However if you took advantage of the S in SVG and had a view box of different dimensions e.g. 4000x2000 or 40x20 (or worse still a different aspect ratio) the shape would not move with the mouse.

See http://www.codedread.com/blog/archives/2005/12/21/how-to-enable-dragging... for a way of overcoming this.

Remi on Nov. 22, 2014, 8:39 p.m.

Thanks for your tutorial but it doesn't work on IOS.

Is it possible to do the same thing with a finger and not a mouse ?

...sorry for my english

Hans Erik Hazelhorst on July 10, 2015, 4:33 p.m.

Thank you for this extremely useful introduction. I have an additional question: how can I pass the result of a drag operation to a URL?

My SVG objects (and all Javascript ) are rendered by a database program, and I need to pass information back to the program using a URL with some parameters. How do I get the resulting coordinates back into parameters in the URL?

Hans Erik Hazelhorst on July 10, 2015, 7:52 p.m.

Thank you for the excellent introduction on making interactive SVGs. Perhaps you tell me how I can retrieve the position of the svg object after the dragging operation, and return it as a part of a URL?

Karl_Heg on Feb. 21, 2016, 10:43 p.m.

The draggable solution in svg or html is a good solution to make
classifications per position X/Y
example:
Horizontal: Fruits/left:600px; Vegetables/800px; Cereales/1000px

Vertical: Poatatoes;Rice;Banana;Mais; Apple;Toatoes;Chicoree

all vertical values: left:400px

set the vertical objects to the right horizontal object/group etc

Is it a possibility to save the changed values/result-document?

Best Regards

Karl_Heg

Alex on March 17, 2016, 8:52 p.m.

Thanks a lot. I needed to animate movement of an svg element and had trouble using jquery or css. Your solution using matrix worked for me.

Rustem Zakiev on Oct. 11, 2016, 4:22 p.m.

Very helpful! Thank you!

Anonymous on Jan. 11, 2017, 7:28 a.m.

Thanks Heaps Peter, great work.

I'm utilising the drag drop and tooltip.

Given some slightly obscure requirements, I've found your work very helpfull in piecing together what I need for my scenario.

Your a champ, thanks again.

Jonathan on June 15, 2017, 1:43 a.m.

Well let me say that even after all these years, you just helped a student for his work of end of studies. :)
So thank you very much

Shashank on June 15, 2017, 6 a.m.

Hi peter,

Can you please also post a tutorial on "How to do a free transfrom of objects/images in SVG similar to Photoshop Free Transform feature"? I could not find any relevent info on this.

Thanks in advance

Shashank

Jaroslav on Dec. 2, 2017, 1:03 a.m.

Hi!

I tried your code with rect elements and with image element. See: http://vysoky.pythonanywhere.com/files/brython/dragndrop-svg-for-peter.html

In google chrome it works fine but in firefox image element's behavior is different. There is some strange firefox self dragging. Sometimes a rectangle starts to behave in the same way. I must select the second rectangle to stop the behavior. I tried it with FF v. 57.0.1 in Linux Mint.

Do you have any idea for correct behavior in firefox too?

Thanks!

Jaroslav

Percival on Dec. 3, 2017, 7:35 p.m.

So ... if I have this set up, so I can build a map, from elements, how do I share that built map with someone else? In other words, how do I save all the transforms that have taken place and then hand that map to someone else, who might want to move things around further, and then hand something back to me?

Thank you.

Henrik on June 13, 2018, 10:02 a.m.

Thanks Peter, this was just what I needed!
Starting from a basic example and building upon that until you got a working solution, and explaining each step it the way to go.

Abdul Mateen on June 20, 2018, 3:13 p.m.

Hi,

First of all, thank you for making this tutorial. I've followed the same steps as you've mentioned but I'm facing an issue.

Reproducing steps:
--------------------------
First time drag the rectangle box somewhere, then try to drag it again or just click it, see it starts from x=0 y=0 coordinates because of the 'offset' value and the 'coord' values are same so, coord.x - offset.x results in 0

Could you please help me fix this issue you can find the fiddle here http://jsfiddle.net/spamrvt1/

Thanks in advance

Peter on June 20, 2018, 9:24 p.m.

Hi Abdul,

Thanks for giving such a well documented bug report. It seems I forgot to show the updated drag function. It should be:

function drag(evt) {
if (selectedElement) {
var coord = getMousePosition(evt);
transform.setTranslate(coord.x - offset.x, coord.y - offset.y);
}
}

The full code can be found here:
https://raw.githubusercontent.com/petercollingridge/code-for-blog/master/svg-interaction/draggable/draggable_final.svg

Abdul Mateen on June 21, 2018, 6:05 a.m.

Hi Peter,

Thanks for such a quick response, it works now :)

Henrik on June 22, 2018, 10:13 a.m.

Hmm, the full code example https://raw.githubusercontent.com/petercollingridge/code-for-blog/master/svg-interaction/draggable/draggable_final.svg, does not contains the multiple touchsupport you included last in your article.
if (evt.touches) {
evt = evt.touches[0];
}

There is also an extra quote-character last on this line:
<rect x="4" y="5" width="8" height="10" fill="#007bff""/>
Under "SVG Setup"

Peter on June 22, 2018, 9:21 p.m.

The code including touch support is at:
https://raw.githubusercontent.com/petercollingridge/code-for-blog/master/svg-interaction/draggable/draggable_mobile.svg
Calling the other file draggable_final.svg is a bit misleading, I'll rename it.

I've removed the extra double quote. Thanks for the feedback.

Jim on Sept. 3, 2018, 12:49 a.m.

how does this work with SVG that is loaded from a file? So in this example, the SVG is defined in the HTML document. I want to be able to load an SVG element from a file, and then scale, rotate and translate it across a specified area. I have tried adapting this code, but it hasn't worked so far. I think maybe I need a new approach?

Peter on Sept. 4, 2018, 1:05 p.m.

I don't quite understand your set-up. Is the SVG an external file, which you're adding into the HTML using an <object>, <iframe> or <embed>? The code you be basically the same, but you might need to make a few changes depend on where the JS code is. I've cover the different ways to use JS and SVG here: http://www.petercollingridge.co.uk/tutorials/svg/interactive/javascript/ but let me know if it's not clear.

Christoph on Sept. 7, 2018, 10:37 a.m.

Thank you very much for this fantastic post. I particular like the step-by-step approach, which really sets the basis for a good understanding. For me this has been the best post on this subject, which I was able to find so far.

Donnie on Sept. 9, 2018, 1:54 a.m.

Have you successfully dragged a foreignObject? I keep getting NaN errors related to offset.

Peter on Sept. 10, 2018, 9:14 p.m.

Hi Donnie, I added an example of dragging a foreignObject here: https://raw.githubusercontent.com/petercollingridge/code-for-blog/master/svg-interaction/draggable/draggable_foreign.svg

I added the draggable class to the foreign object element and then added a style foreignObject * { pointer-events: none; } so the mouse events ignored any of the other elements inside the foreignObject. I don't think you can transform them directly as non SVG elements won't have transform attributes.

Maja on Nov. 7, 2018, 1:31 p.m.

I also just wanted to say thank you for this really useful tutorial. It saved me a lot of time having all of this on a silver platter.

Leave a comment

cancel reply