1. Mouse & Touch Events

To get the mouse or touch position, we first need to add event listeners on our SVG. We can use the Pointer Events to handle all kind of pointers (mouse/touch/stylus/...) but those events are not yet supported by all browsers. We will need to add some fallback to make sure all users will be able to drag the SVG.

Once the page is ready and waiting for any user interactions, we can start handling the mousedown/touchstart events to save the original coordinates of the pointer and create a variable to let us know if the pointer is down or not.

// This variable will be used later for move events to check if pointer is down or not
var isPointerDown = false;
// This variable will contain the original coordinates when the user start pressing the mouse or touching the screen
var pointerOrigin = {
x: 0,
y: 0
};
// Function called by the event listeners when user start pressing/touching
function onPointerDown(event) {
isPointerDown = true; // We set the pointer as down
// We get the pointer position on click/touchdown so we can get the value once the user starts to drag
var pointerPosition = getPointFromEvent(event);
pointerOrigin.x = pointerPosition.x;
pointerOrigin.y = pointerPosition.y;
}

2. Calculate Mouse Offsets

Now that we have the coordinates of the original position where the user started to drag inside the SVG, we can calculate the distance between the current pointer position and its origin. We do this for both the X and Y axis and we apply the calculated values on the viewBox.

// We save the original values from the viewBox
var viewBox = {
x: 0,
y: 0,
width: 500,
height: 500
};
// The distances calculated from the pointer will be stored here
var newViewBox = {
x: 0,
y: 0
};
// Function called by the event listeners when user start moving/dragging
function onPointerMove (event) {
// Only run this function if the pointer is down
if (!isPointerDown) {
return;
}
// This prevent user to do a selection on the page
event.preventDefault();
// Get the pointer position
var pointerPosition = getPointFromEvent(event);
// We calculate the distance between the pointer origin and the current position
// The viewBox x & y values must be calculated from the original values and the distances
newViewBox.x = viewBox.x - (pointerPosition.x - pointerOrigin.x);
newViewBox.y = viewBox.y - (pointerPosition.y - pointerOrigin.y);
// We create a string with the new viewBox values
// The X & Y values are equal to the current viewBox minus the calculated distances
var viewBoxString = `${newViewBox.x} ${newViewBox.y} ${viewBox.width} ${viewBox.height}`;
// We apply the new viewBox values onto the SVG
svg.setAttribute('viewBox', viewBoxString);
document.querySelector('.viewbox').innerHTML = viewBoxString;
}

If you don't feel comfortable with the concept of viewBox, I would suggest you first read this great article by Sara Soueidan.

3. Save Updated viewBox

Now that the viewBox has been updated, we need to save its new values when the user stops dragging the SVG.

This step is important because otherwise we would always calculate the pointer offsets from the original viewBox values and the user will drag the SVG from the starting point every time.

function onPointerUp() {
// The pointer is no longer considered as down
isPointerDown = false;
// We save the viewBox coordinates based on the last pointer offsets
viewBox.x = newViewBox.x;
viewBox.y = newViewBox.y;
}

4. Handle Dynamic Viewport

If we set a custom width on our SVG, you may notice while dragging on the demo below that the bird is moving either faster or slower than your pointer.

On the original demo, the SVG's width is exactly matching its viewBox width. The actual size of your SVG may also be called viewport. In a perfect situation, when the user is moving their pointer by 1px, we want the viewBox to translate by 1px.

But, most of the time, the SVG has a responsive size and the viewBox will most likely not match the SVG viewport. If the SVG's width is twice as big than the viewBox, when the user moves their pointer by 1px, the image inside the SVG will translate by 2px.

To fix this, we need to calculate the ratio between the viewBox and the viewport and apply this ratio while calculating the new viewBox. This ratio must also be updated whenever the SVG size may change.

[Bonus] Optimizing the code

To make our code a bit shorter, there are two very useful concepts in SVG we could use.

SVG Points

The first concept is to use SVG Points instead of basic Javascript objects to save the pointer's positions. After creating a new SVG Point variable, we can apply some Matrix Transformation on it to convert the position relative to the screen to a position relative to the current SVG user units.

Check the code below to see how the functions getPointFromEvent() and onPointerDown() have changed.

// Create an SVG point that contains x & y values
var point = svg.createSVGPoint();
function getPointFromEvent (event) {
if (event.targetTouches) {
point.x = event.targetTouches[0].clientX;
point.y = event.targetTouches[0].clientY;
} else {
point.x = event.clientX;
point.y = event.clientY;
}
// We get the current transformation matrix of the SVG and we inverse it
var invertedSVGMatrix = svg.getScreenCTM().inverse();
return point.matrixTransform(invertedSVGMatrix);
}
var pointerOrigin;
function onPointerDown(event) {
isPointerDown = true; // We set the pointer as down
// We get the pointer position on click/touchdown so we can get the value once the user starts to drag
pointerOrigin = getPointFromEvent(event);
}

By using SVG Points, you don't even have to handle transformations applied on your SVG! Compare the following two examples where the first is broken when a rotation is applied on the SVG and the second example uses SVG Points.

SVG Animated Rect

The second unknown concept in SVG we can use to shorten our code, is the usage of Animated Rect.

Because the viewBox is actually considered as an SVG Rectangle (x, y, width, height), we can create a variable from its base value that will automatically update the viewBox if we update this variable.

See how easier it is now to update the viewBox of our SVG!

// We save the original values from the viewBox
var viewBox = svg.viewBox.baseVal;
function onPointerMove (event) {
if (!isPointerDown) {
return;
}
event.preventDefault();
// Get the pointer position as an SVG Point
var pointerPosition = getPointFromEvent(event);
// Update the viewBox variable with the distance from origin and current position
// We don't need to take care of a ratio because this is handled in the getPointFromEvent function
viewBox.x -= (pointerPosition.x - pointerOrigin.x);
viewBox.y -= (pointerPosition.y - pointerOrigin.y);
}

Sure !
You can check those two demos made by Sarah Drasner.
Both are using media-queries to change the look of the SVG’s based on the viewport. The first one is also updating the viewBox of the SVG to display a specific area per screen size !

Cool demo. I recently found this gem of an svg map (https://www.mallofamerica.com/directory — click on map view) and haven’t yet taken the time to figure out how it works, but this is a great start. I love how clean it is and the responsiveness of it works fabulously.

From what I saw, the viewBox is not updated while dragging but instead there is a CSS transform applied on the container of the whole map to make sure the HTML linked to the stores is also dragged :)
But it’s a great interactive map anyway !

This comment thread is closed. If you have important information to share, please contact us.

Related

How do you stay up to date in this fast⁠-⁠moving industry?

A good start is to sign up for our weekly hand-written newsletter. We bring you the best articles and ideas from around the web, and what we think about them.

👋

CSS-Tricks* is created, written by, and maintained by Chris Coyier and a team of swell people. It is built on WordPress and powered up by Jetpack. It is made possible through sponsorships from products and services we like.