Select a portion of an element by clicking and dragging
Written byPhuoc Nguyen
Created
12 Nov, 2023
Last updated
15 Nov, 2023
Tags
React click-and-drag
Throughout this series, we've been exploring how to make existing elements draggable and how to limit their movement within a container. But what about situations where the draggable elements don't exist yet? What if users need to create their own draggable elements by selecting a portion of the container with their mouse and generating a draggable element from that selection?
This is where click and drag selection comes in. It's a quick, precise, and intuitive way for users to select elements and generate draggable components. By clicking and dragging the mouse over the desired area, users can create a selection that can then be turned into a draggable element.
The benefits of click and drag selection are numerous. It's fast and efficient, allowing users to select elements with ease. It also provides precise control over the selected area, enabling users to fine-tune their selections to include only what's necessary. Plus, it's a natural interaction pattern that many users are already familiar with, making it easy to understand and use.
Click and drag selection can be used in a variety of applications, from image cropping and text highlighting to selecting table cells and resizing UI components. By incorporating this functionality into our web applications, we can create interfaces that are more intuitive and efficient for our users.
In this post, we'll explore how to implement click and drag selection with React, so that we can provide our users with a familiar and intuitive way to interact with our interfaces.
#Setting up the layout
Let's say we want to add click-and-drag functionality to an element with the class
`container`
:tsx
<div className="container">...</div>
To make it clear that users can click and drag the container, we need to add some basic styles. We can do this by setting the
`cursor`
property of the container to `crosshair`
. This changes the cursor icon when hovering over the container, letting users know that it can be clicked and dragged.It's also important to set
`touch-action: none`
on the container element. This ensures that mobile browsers don't interpret touch events as scrolling gestures, which could interfere with our click and drag functionality.By adding these styles, users will be able to easily identify which elements are draggable and interact with them smoothly.
css
.container {
cursor: crosshair;
touch-action: none;
}
#Handling events
As with our previous tutorials in this series, we'll be taking the same approach. We'll create a custom hook called
`useClickToSelect`
to enable click and drag functionality.If you recall the hook we used to make an element draggable, the structure of
`useClickToSelect`
will be quite similar.tsx
const useClickToSelect = () => {
const [node, setNode] = React.useState<HTMLElement>();
const [{ dx, dy }, setOffset] = React.useState({
dx: 0,
dy: 0,
});
const [{ startX, startY}, setStartPosition] = React.useState({
startX: 0,
startY: 0,
});
const ref = React.useCallback((nodeEle) => {
setNode(nodeEle);
}, []);
const handleMouseDown = React.useCallback((e: React.MouseEvent) => {
// ...
}, [node, dx, dy]);
const handleTouchStart = React.useCallback((e: React.TouchEvent) => {
// ...
}, [node, dx, dy]);
React.useEffect(() => {
node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("touchstart", handleTouchStart);
return () => {
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("touchstart", handleTouchStart);
};
}, [node, dx, dy]);
return [ref, dx, dy, startX, startY];
};
The hook we're working with returns an array with five items. First, we have a reference that we attach to the target element using the
`ref`
attribute. Next, we have two items that represent the distance the mouse has been moved horizontally and vertically.The last two items are new compared to the hook we created earlier to make an element draggable. They represent the starting point position where users click on the target element.
When a user clicks on the target element, the
`handleMouseDown`
function is called. This function calculates the position of the mouse relative to the top-left corner of the element using the `getBoundingClientRect()`
method and saves it in `startX`
and `startY`
variables.We reset the
`dx`
and `dy`
values in the state by calling `setOffset`
with an object that contains zeros for both properties. This ensures that the next time a user starts dragging our new element, it starts from its original position instead of continuing from where it was left off during the previous drag movement. In other words, we're making sure our new element doesn't retain any "memory" of its previous position.The function then calculates the starting point of the drag movement based on the mouse coordinates. Finally, the function updates the state by calling
`setStartPosition`
with an object that contains `startX`
and `startY`
values. We use these values later to calculate how much to move our new draggable element.Here's how we handle the
`mousedown`
event:ts
const handleMouseDown = React.useCallback((e: React.MouseEvent) => {
const eleRect = node.getBoundingClientRect();
const startRelativePos = {
startX: e.clientX - eleRect.left,
startY: e.clientY - eleRect.top,
};
setOffset({ dx: 0, dy: 0 });
setStartPosition(startRelativePos);
const startPos = {
x: e.clientX,
y: e.clientY,
};
}, [node, dx, dy]);
The
`handleMouseMove`
function updates the position of our draggable element as the user drags it.First, it calculates the horizontal and vertical distances between the starting point of the drag movement and the current mouse position. These distances are saved in variables
`dx`
and `dy`
.Next, we calculate the maximum allowed values for
`dx`
and `dy`
based on the container size of the element. We use the `clamp`
function to ensure that our element stays within its container.Finally, we update our state by calling
`setOffset`
with a new object that contains updated values for `dx`
and `dy`
. This makes our element move in response to the user's drag movements.ts
const handleMouseMove = (e: React.MouseEvent) => {
let dx = e.clientX - startPos.x;
let dy = e.clientY - startPos.y;
const maxX = eleRect.width - startRelativePos.startX;
const maxY = eleRect.height - startRelativePos.startY;
dx = clamp(dx, 0, maxX);
dy = clamp(dy, 0, maxY);
setOffset({ dx, dy });
};
When the user releases the mouse button, the
`handleMouseUp`
function is called. This function removes event listeners for `mousemove`
and `mouseup`
events on the `document`
object, which stops tracking the user's drag movements after releasing the mouse button. This is the same as what we did when making an element draggable.ts
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
#Tracking the selection state
In many real-life situations, the selected portion can be used for another purpose. For example, in an image cropper, users can drag or resize the selected portion to crop the image. To enable this functionality, the
`useClickToSelect`
hook needs to inform the outside world when the user releases the mouse, allowing for additional tasks to be performed with the selected portion.To accomplish this, we will create an enumeration to represent the state of the selection.
ts
enum Selection {
None = 'None',
Clicked = 'Clicked',
Dragging = 'Dragging',
Selected = 'Selected',
};
The
`None`
value is what the system uses as a default when users haven't interacted with the target element. When users click on the element and start moving their selection around, the `Clicked`
and `Dragging`
values are activated, respectively. Finally, when users let go of the mouse, the `Selected`
value is triggered.To keep track of the selection state, we need to make some changes to the
`useClickToSelect`
function. First, we'll create an internal state that starts as `None`
.ts
const [selection, setSelection] = React.useState(Selection.None);
When users click and release the mouse buttons, the selection state needs to be updated. This can be achieved by making minor adjustments to the
`mousedown`
, `mousemove`
and `mouseup`
event handlers.ts
const handleMouseDown = () => {
setSelection(Selection.Clicked);
};
const handleMouseMove = () => {
setSelection(Selection.Dragging);
};
const handleMouseUp = () => {
setSelection(Selection.Selected);
};
To let the world know about the selection state, it needs to be included in the array that is returned.
ts
const useClickToSelect = () => {
return [ref, dx, dy, startX, startY, selection];
};
#Using the custom hook
As previously stated, the custom hook will provide you with an array of six items.
ts
const [clickToSelectRef, dx, dy, startX, startY, selection] = useClickToSelect();
The first thing we need is a reference to the target element. In this case, we'll attach the first item to the container using the
`ref`
attribute.tsx
<div className="container" ref={clickToSelectRef}>
...
</div>
We'll be using the remaining items to generate a preview of a selected portion. To create the preview, we'll use a
`div`
element with the class name `selection`
. This `div`
will only appear when the user drags the mouse over an element. We'll set its position using CSS transforms and its width/height using the `dx`
and `dy`
values from our custom hook.tsx
{
selection === Selection.Dragging && (
<div
className="selection"
style={{
transform: `translate(${startX}px, ${startY}px)`,
width: `${dx}px`,
height: `${dy}px`,
}}
/>
)
}
It's important to note that the selection is positioned absolutely within the container. To do this, we need to set the container's CSS property position to
`relative`
. This ensures that the selection is positioned relative to its parent container.Next, we set the selection's CSS property position to
`absolute`
. This takes it out of the normal document flow and positions it relative to its closest positioned ancestor element (in this case, our container).Lastly, we need to set its
`top`
and `left`
properties to 0 so that it starts at the top-left corner of our container.css
.container {
position: relative;
}
.selection {
position: absolute;
top: 0;
left: 0;
}
To make this
`div`
more visible and appealing, we can add various styles like border, background color, opacity, and more. This preview helps users visualize what they're selecting as they drag the mouse.css
.selection {
border: 2px dashed rgb(203 213 225);
}
Take a look at the demo below. All you have to do is click on the container and drag your mouse around. You'll see the selection generated in real-time. Give it a try!
#Conclusion
To wrap things up, we've successfully created a custom hook called
`useClickToSelect`
that lets users select an area within a container by clicking and dragging. This hook is similar to the one we made earlier for dragging elements, but with some extra features.We've covered how to calculate the mouse's position relative to the element, how to update the state based on drag movements, and how to create a selection preview using CSS transforms.
This is just one example of how custom hooks can be used to enhance user experience and add new functionalities to React applications. By creating reusable code snippets like this, we can save time, reduce code repetition, and make our applications easier to maintain.
In the upcoming posts, we'll experiment with this hook in real-life examples. Stay tuned for more!
#See also
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