Make a given element draggable
Written byPhuoc Nguyen
Created
27 Oct, 2023
Tags
draggable element, React drag drop
Let's start our journey with a simple step: allowing users to drag a given element.
One example where this feature can be useful is in a website builder. Here, users can drag and drop various components, like images, text boxes, and buttons, onto a canvas to create their own unique web page layout. This simplifies the process of designing a website for those without coding experience.
Another example where draggable elements come in handy is in presentation tools. Users can drag and drop multimedia components, such as images, videos, and graphs, onto a slide to create their own unique presentation layout. This simplifies the process of designing a presentation for those without design experience. And if a user wants to change the order or position of any element on their slide, they can easily do so by dragging and dropping the element to its new location.
In this post, we'll learn how to create a draggable element in React. So, let's get started!
#Tracking element movement
Picture this: you drag an element and it triggers three different events. The
`mousedown`
event is triggered when you click on an element, the `mousemove`
event is triggered when you move your mouse around, and the `mouseup`
event is triggered when you release the mouse.To make an element draggable, we can handle these events and track how far the element has been moved. Then, we can update the element's position accordingly.
But first, we need to position the element absolutely within its container. To do this, we need to add
`position: relative;`
to the container and `position: absolute;`
to the draggable element. This makes the draggable element's position relative to its container.css
.container {
position: relative;
}
.draggable {
position: absolute;
cursor: move;
user-select: none;
}
In this example, we set the cursor style to
`move`
on the target element to indicate that it's draggable. This changes the cursor icon to a four-sided arrow when users hover over it, letting them know they can click and drag the element.Adding
`user-select: none;`
prevents users from accidentally selecting text within the draggable element while moving it, which is a nice touch.After styling our element, it's time to handle the events. We'll need a few internal states to track mouse movements.
ts
const [{ dx, dy }, setOffset] = React.useState({
dx: 0,
dy: 0,
});
const [startPos, setStartPos] = React.useState({
x: 0,
y: 0,
});
const [isMouseDown, setMouseDown] = React.useState(false);
When we click the mouse button on a draggable element, we need to store its initial position with
`startPos`
. This helps us calculate the distance between the original and new positions as we drag the element.As we drag,
`dx`
and `dy`
tell us the difference between the current mouse position and the initial click position. We update these values with every mouse move event, so we can reposition the element relative to its parent container.Lastly,
`isMouseDown`
is a simple boolean that tells us whether the mouse button is pressed or not. We use this to determine if we should be handling mouse move events.We use the
`useRef()`
hook to create a reference to the target element. This allows us to retrieve the element later on.We'll be using React ref a lot in this series. If you're not familiar with it, we recommend going through our comprehensive series that covers React refs with detailed examples.
To attach the reference to the element via the
`ref`
attribute and manage the events, we use the following code. Don't worry about the `handleMouseDown`
, `handleMouseMove`
, and `handleMouseUp`
functions just yet. We'll take a closer look at them shortly.tsx
const eleRef = React.useRef();
const handleMouseDown = (e: React.MouseEvent) => {
...
};
const handleMouseMove = (e: React.MouseEvent) => {
...
};
const handleMouseUp = () => {
...
};
// Render
return (
<div className="container">
<div
className="draggable"
ref={eleRef}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
Drag me
</div>
</div>
);
When the user clicks on the draggable element, it triggers the
`mousedown`
event. We need to calculate the offset between the mouse position and the top-left corner of the draggable element. This helps us update the position of the element based on how far it has been dragged from its original position.In our code example, we store the starting position of the mouse in
`startPos.x`
and `startPos.y`
, which we'll use later in our `handleMouseMove`
function to calculate how far the mouse has moved since it was clicked.Here's how we handle the
`mousedown`
event:ts
const handleMouseDown = (e: React.MouseEvent) => {
setMouseDown(true);
setStartPos({
x: e.clientX - dx,
y: e.clientY - dy,
});
};
When the user moves their mouse, it triggers the
`mousemove`
event. We check if the element is being dragged by looking at the value of `isMouseDown`
. If it's `true`
, we calculate how far the mouse has moved from its starting position using `e.clientX - startPos.x`
and `e.clientY - startPos.y`
, which are assigned to `dx`
and `dy`
.Then, we update the position of our draggable element by setting its
`transform`
style to `translate(dx, dy)`
. This moves the element by `dx`
pixels horizontally and `dy`
pixels vertically relative to its starting position.Good practiceWhen moving elements around, it's better to use the`transform`
style with`translate(dx, dy)`
instead of setting the`top`
and`left`
properties directly. Why? Because setting`top`
and`left`
forces the browser to recalculate the element's layout, which can be slow and inefficient if done frequently.On the other hand,`transform: translate(dx, dy)`
creates a new layer for our element, separate from its parent container. This means that when we move our element, only that layer needs to be repainted, making the process smoother and faster.Another advantage of using`transform`
is that it preserves existing transitions or animations on our element. If we were to use`top`
and`left`
, we would have to remove any existing transitions or animations before updating those properties, then reapply them afterward. With`transform`
, this isn't necessary as it doesn't affect any other styles on the element.In summary, using`transform: translate(dx, dy)`
is a more efficient way to move elements within their container, while preserving performance and avoiding unnecessary layout recalculations.
Finally, we update our offset state with the new values of
`dx`
and `dy`
. This allows us to keep track of how far our element has been dragged from its original position, so we can continue to update its position correctly as long as the user keeps dragging it.Here's an example of how the
`mousemove`
event handler could look:ts
const handleMouseMove = (e: React.MouseEvent) => {
const ele = eleRef.current;
if (!ele || !isMouseDown) {
return;
}
// How far the mouse has been moved
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
// Set the position of element
ele.style.transform = `translate(${dx}px, ${dy}px)`;
// Reassign the position of mouse
setOffset({ dx, dy });
};
When the user lets go of the mouse button, it triggers the
`mouseup`
event. We handle this event by setting `isMouseDown`
to `false`
. This tells us that the element is no longer being dragged, and we can stop updating its position. It's like putting down a toy you were playing with - once you let go, you're done.ts
const handleMouseUp = () => {
setMouseDown(false);
};
We don't need to do anything else in our
`handleMouseUp`
function because we've already updated our state and CSS styles in the previous two functions. However, it's important to note that we should always handle this event to ensure that our application works properly and doesn't encounter any unexpected bugs or errors.Now, let's take a look at how our implementation has progressed so far:
Dragging the element works fine when done slowly. But when you try to drag it faster, the element doesn't get positioned correctly. Don't worry, we'll fix this issue in the next section.
#Making dragging faster work
In the demo above, you may have noticed that as soon as you move the mouse faster, it can go outside of the element boundary. At that point, the element's
`mousemove`
event won't trigger and the element position won't be updated.To fix this, we need to handle the
`mousemove`
and `mouseup`
events for the whole document, rather than just the specific target. We can accomplish this by using React's `useEffect()`
hook to attach a handler to a particular event of the `document`
.ts
React.useEffect(() => {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isMouseDown]);
It's important to note that we pass the
`isMouseDown`
state as a dependency of `useEffect()`
. This ensures that the `handleMouseMove`
function works properly with the most up-to-date value of the state.Good practiceWhen using the`useEffect`
hook, it's important to remember to remove any event handlers that are inside the returned value. This ensures that everything is properly cleaned up and prevents memory leaks from happening when your component unmounts. Don't forget this crucial step!
Let's test the element by dragging it quickly and see if the new implementation resolves the issue. Success! The issue is fixed.
#Simplifying event handling
In our previous approach, we used a flag called
`isMouseDown`
to determine if the user clicked the element. The `mousemove`
and `mouseup`
events were handled conditionally based on that flag. However, this approach can become problematic when dealing with multiple events that require the same approach.To improve our approach, we can remove the flag altogether and instead attach the event handlers only inside the
`mousedown`
event. This simplifies the code and makes it more efficient. With this new approach, we can easily handle multiple events without the need for additional flags or conditional statements.ts
const handleMouseDown = (e: React.MouseEvent) => {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
Because we don't use the
`useEffect`
hook to attach these handlers, they need to be removed when the `mouseup`
event is triggered.ts
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
You don't need to manage the
`isMouseDown`
flag anymore. If you examine the `handleMouseDown`
function in the demo below, you'll notice that we also eliminate the use of the `startPost`
state.Now, let's take a moment to appreciate the progress we've made today!
#Conclusion
To wrap things up, we've successfully created a draggable component using React and TypeScript. We've learned how to handle mouse events like
`mousedown`
, `mousemove`
, and `mouseup`
to update the position of our element based on the user's input.In addition, we've explored best practices for moving elements within their container, which can help us avoid unnecessary layout recalculations and improve performance.
By simplifying our event handling code, we've made it more efficient and easier to manage multiple events that require similar approaches.
Overall, this implementation provides an excellent starting point for creating more complex drag-and-drop interactions in your projects. Armed with these techniques, you can create highly interactive user interfaces that elevate the overall user experience.
#See also
- Make a draggable element in vanilla JavaScript
Questions? 🙋
Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Recent posts ⚡
Newsletter 🔔
If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn