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

Constraining movement

Several people have asked how to restrict the movement of items when dragging. At the very least it would make sense to prevent items from being dragged off screen.

The first thing we need to do is to define the boundary we're going to restrict items' movement to. Here I'm using four variables to define a rectangular boundary. If you want to use a more complex shape then you'll have to look into more complex collision detection.

var boundaryX1 = 10.5;
var boundaryX2 = 30;
var boundaryY1 = 2.2;
var boundaryY2 = 19.2;

In the startDrag function, we test whether the selected element has the class "confine". This is only required if you want some items to be unconstrained.

Then we use the built-in getBBox method to find the bounding box of the element. This returns an object with attributes, x, y, width and height for a box that surrounds the item. We can use this to find the bounding coordinates for the element relative to the boundary.

confined = selectedElement.classList.contains('confine');
if (confined) {
  bbox = selectedElement.getBBox();
  minX = boundaryX1 - bbox.x;
  maxX = boundaryX2 - bbox.x - bbox.width;
  minY = boundaryY1 - bbox.y;
  maxY = boundaryY2 - bbox.y - bbox.height;
}

Note that getBBox doesn't take into account transformations on the element, so it's best to not have elements with existing transformations. You can either apply the transformations to the element directly or wrap everything in a group.

Then, in the drag method, we calculate the position the element is dragged to, and ensure it doesn't exceed the boundary before applying it

var dx = coord.x - offset.x;
var dy = coord.y - offset.y;

if (confined) {
  if (dx < minX) { dx = minX; }
  else if (dx > maxX) { dx = maxX; }
  if (dy < minY) { dy = minY; }
  else if (dy > maxY) { dy = maxY; }
}

transform.setTranslate(dx, dy);

Here's an example of the code in action. The dark grey box represents the restricted area. The ellipse star and word all have the "confine" class name so they cannot leave the box. The rectangle and squiggle don't have the class name, so can move freely.

The complete code can be found here.

Drag