← Back toDrag and drop in React

Craft an image cropper

Written byPhuoc Nguyen
Created
10 Nov, 2023
Tags
React image cropper
An image cropper is a powerful tool that lets you select and crop specific areas of an image. This is especially useful for web development since images need to be optimized for different screen sizes and resolutions. Responsive design is becoming increasingly important, and it's crucial to ensure that images are high quality and properly sized for different devices. An image cropper simplifies this process by allowing you to select the most important part of an image and create smaller versions of the original. This also helps to reduce page load times, which is essential for improving website performance.
Image cropping is also valuable in other scenarios. E-commerce websites need product images displayed in a uniform size. By cropping product images to fit a specific aspect ratio, they appear consistent throughout the website. Social media platforms like Instagram and Facebook require users to upload images of specific dimensions. By allowing users to crop their images before uploading them, these platforms ensure that all uploaded photos look good on any device and are uniform.
Professional photographers also use image cropping to enhance composition or focus on specific elements of an image. By selecting the most important part of an image and cropping it, photographers can create more impactful and visually appealing photographs.
To summarize, image cropping is a versatile tool used in various industries to improve the visual appeal of images and optimize them for different screen sizes and resolutions. In this post, we'll learn how to create an image cropper using React.

Getting the layout ready

When setting up the layout for an image cropper component, there are two main elements to consider: the original image and the cropping area. The original image is displayed in a container, or "cropper", where users can select the specific area they want to crop.
The second element, the "cropping area", represents the selected portion of the original image that will be cropped and saved. This area is usually a rectangular box that can be resized and moved around on top of the original image.
tsx
<div className="cropper">
<div className="cropper__image">
<img src="..." />
</div>
<div className="cropper__cropping" />
</div>
To properly align the original image within the cropper tool, we use CSS. First, we set the position of the image container to `absolute`. Then, we align it to the top-left corner of the cropper container by setting both the `top` and `left` properties to 0. Finally, we set the `width` to 100% to make sure it fills up the entire cropper container.
This ensures that users can easily select and crop the original image without any part being cut off.
css
.cropper {
position: relative;
overflow: hidden;
width: 100%;
}
.cropper__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
To position the cropping area, we use CSS to set its position to `absolute`. This allows us to easily move it around on top of the original image. We also set its initial height and width using the `height` and `width` properties. Finally, we align it with the top-left corner of the cropper container by setting both the `top` and `left` properties to 0. This ensures that the cropping area starts off in the correct place and can be resized or moved as needed by users.
css
.cropper__cropping {
height: 8rem;
width: 8rem;

position: absolute;
top: 0;
left: 0;
}
To make it easier for users to interact with the cropping area, we can use CSS to add a dashed border around it. This border is typically white or another high-contrast color to ensure that it stands out against the original image. By using the `outline` property, we can also provide a visual indicator of where the cropping area starts and stops. This is especially useful for users who need to select a specific portion of an image.
css
.cropper__cropping {
outline: 1px dashed #fff;
}

Making the original image fit its container

Since the original image can come in various sizes, we need to fit it within the container element. To accomplish this, we'll handle the `onLoad` event of the image and calculate its aspect ratio. This ensures that the image always fits perfectly within its container.
tsx
<div className="cropper" ref={containerRef}>
<div className="cropper__image">
<img src="..." onLoad={handleImageLoad} />
</div>
</div>
The `handleImageLoad` function is crucial for fitting the original image within its container. When the image is loaded, this function is called and it calculates the aspect ratio of the image to ensure it fits snugly in its container while maintaining its original dimensions.
First, we use React's `useRef` hook to get a reference to the container element. This allows us to access and manipulate its properties as needed. Then, we calculate the natural width and height of the image using the `target.naturalWidth` and `target.naturalHeight` properties.
Next, we divide these two values to get the aspect ratio of the image. We then use the `getBoundingClientRect()` method to get the width of the container element and calculate its height based on this aspect ratio.
Finally, we set the height of the container element using inline styles and set the width of the image to match that of its parent container. This ensures that our original image fits perfectly within its container while maintaining its aspect ratio. This is critical for creating an effective image cropper tool with React.
Here's how we handle the `onLoad` event of the image.
ts
const handleImageLoad = (e) => {
const container = containerRef.current;
const naturalWidth = e.target.naturalWidth;
const naturalHeight = e.target.naturalHeight;
const ratio = naturalWidth / naturalHeight;

const containerWidth = container.getBoundingClientRect().width;
container.style.height = `${containerWidth / ratio}px`;
e.target.style.width = `${containerWidth}px`;
};
By fitting our original image within its container element, we can make sure that it displays correctly and can be cropped by users with ease. This technique also helps us maintain high visual quality while optimizing images for various screen sizes and resolutions.

Enabling drag and drop for the cropping area

Users need to be able to move the cropping area around to select the desired portion of the image. We can achieve this functionality by making the cropping area draggable using the `useDraggable` hook we created earlier.
The hook also ensures that the cropping area stays within its container, preventing users from dragging it outside of the designated area. Using this custom hook is straightforward. It returns a reference to the element we want to make draggable, which we can then attach to the target element using the `ref` attribute.
tsx
const [draggableRef] = useDraggable();

// Render
<div className="cropper__cropping" ref={draggableRef} />
Let's take a look at the first demo. You can drag the cropping area around, but it will only move within the container. Give it a try!
I'd like to give credit to Tron Le for the beautiful image used in this post. Thank you for capturing the beauty of Ho Chi Minh City, my hometown.

Previewing the cropped image

When users want to crop a photo, they select a portion of the original image. Everything outside of the selected area is obscured by an overlay. This overlay provides a clear visual indicator of what part of the image will be cropped and what part will be discarded.
To create the overlay, we need to add an element right below the original image.
tsx
<div className="cropper">
<div className="cropper__image">
...
</div>
<div className="cropper__overlay" />
<div className="cropper__cropping" />
</div>
To position the overlay element, we use CSS to set its position to `absolute`. This allows us to place it on top of the original image. We then align it with the top-left corner of the cropper container by setting both the `top` and `left` properties to 0. Finally, we set its height and width to match that of the original image using the `height` and `width` properties. This way, the overlay element covers the entire image, enabling us to crop it easily.
css
.cropper__overlay {
position: absolute;
top: 0;
left: 0;

height: 100%;
width: 100%;
}
To make it clear which part of the image will be cropped, we use a dark background color and set its `opacity` property to 0.5. This creates a semi-transparent black layer over the original image, allowing for a clear visual of the crop area.
css
.cropper__overlay {
background: #000;
opacity: 0.5;
}
By positioning the overlay element in this way, we make sure that anything outside the selected cropping area is hidden by a semi-transparent black layer. This gives users a clear visual cue about what part of the image will be cropped and what will be discarded.
However, the overlay also covers the cropping area. To solve this problem, we add a new preview element containing the same image as the original, but without the overlay. This ensures that the cropped area remains visible and unobstructed.
tsx
<div className="cropper" ref={containerRef}>
<div className="cropper__cropping">
<div className="cropper__preview">
<img
src={imageUrl}
ref={previewImageRef}
/>
</div>
</div>
</div>
The preview element is a part of the cropping area and has an `img` element inside it with the original image URL. We use CSS to set the `height` and `width` of the image to 100% and add `overflow: hidden` to show only the selected section of the image. This way, any part of the original image outside the cropping area is hidden from view.
css
.cropper__preview {
height: 100%;
width: 100%;
overflow: hidden;
}
To keep things consistent, we set the width of the preview image to match the width of the container, just like we did with the original image. Check out the updated `handleImageLoad` function below, which takes care of the `onLoad` event for the original image:
tsx
const handleImageLoad = (e) => {
const container = containerRef.current;
const previewImage = previewImageRef.current;
const containerWidth = container.getBoundingClientRect().width;
previewImage.style.width = `${containerWidth}px`;
};
To display the selected portion of the original image in the preview area, we use CSS to adjust the position of the `img` element within the preview container. This is done by setting the `transform` property to negative values of `dx` and `dy`, which represent the horizontal and vertical movement of the cropping area.
We also updated the `useDraggable` hook to return the `dx` and `dy` properties, in addition to the draggable element reference.
tsx
const [draggableRef, dx, dy] = useDraggable();

// Render
<div className="draggable" ref={draggableRef}>
<div className="cropper__preview">
<img
style={{
transform: `translate(${-dx}px, ${-dy}px)`,
}}
/>
</div>
</div>
To display only the selected portion of an image within our preview container, we use negative values for `dx` and `dy`. This moves the image in the opposite direction of the cropping area. By doing this, users can easily see a clearer representation of what their cropped image will look like before they finalize their selection.
Take a look at the demo below. You can move the cropping area around to see a real-time preview of the selected portion.

Allowing users to resize the cropping area

Let's make our image cropper even more user-friendly by allowing users to resize the cropping area. This gives them greater control over which part of the image they want to keep.
To achieve this, we'll use a custom hook called `useResizable`. This hook lets us attach resizing functionality to any element in our React app. You can check out this post to see how we implemented this hook previously.
To use the hook, all we have to do is take the reference it returns and attach it to the cropping area using the `ref` attribute.
tsx
const [resizableRef] = useResizable();

// Render
<div className="resizable" ref={resizableRef}>
<div className="resizer resizer--r" />
<div className="resizer resizer--b" />
</div>
We've made some improvements to our cropping feature, which allows users to adjust the dimensions of their image by dragging the edges or corners of the cropping area. To make this more user-friendly, we've added small circles at the right and bottom sides of the cropping area that users can click and drag to resize.
To prevent users from resizing the cropping area beyond the container, we've added some restrictions similar to the drag-and-drop feature. We've also included a code snippet below that shows how we handle the `mousemove` event when users move the resizer handlers.
tsx
const handleMouseMove = (e: React.MouseEvent) => {
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;

const parent = node.parentElement;
const parentRect = parent.getBoundingClientRect();
const eleRect = node.getBoundingClientRect();
const newWidth = clamp(
w + dx,
0,
parentRect.width - (eleRect.left - parentRect.left)
);
const newHeight = clamp(
h + dy,
0,
parentRect.height - (eleRect.top - parentRect.top)
);

direction === Direction.Horizontal
? (node.style.width = `${newWidth}px`)
: (node.style.height = `${newHeight}`);
};
The `handleMouseMove` function is a part of the `useResizable` hook. It makes sure that users can't resize the cropping area outside of its container.
When a user tries to resize the cropping area, this function calculates how much the width or height needs to change based on their mouse movement. But, before applying these changes, we have to ensure that the resizing handles don't go beyond the container's boundaries.
To do this, we use the `getBoundingClientRect()` method to get information about the target element and its parent container. Then, we calculate new values for `newWidth` and `newHeight`, which represent the new dimensions of our resizable element after the user's actions. We use a `clamp` function with three arguments (current value, minimum value, and maximum value) to keep our resizable element within the container's boundaries.
Finally, we apply these new values back to our target element using CSS styles. This implementation ensures that users can only resize their cropping area within the container. This way, their final cropped image will always fit neatly inside it.

Combining dragging and resizing functionalities

We've created two distinct hooks: `useDraggable` and `useResizable`. The `useDraggable` hook makes an element draggable, while `useResizable` makes it resizable. Each hook returns a reference that you can attach to any element. However, if you attempt to use both hooks on the same cropping area with the same reference, you're inviting problems.
ts
const [draggableRef] = useDraggable();
const [resizableRef] = useResizable();
We're currently facing a challenge: how to create an element that utilizes all the different functionalities provided by various refs. We can't use the refs we've already returned for the same `ref` attribute of the target element.
However, there's good news: we have a solution! It's called merging refs. By combining multiple refs into a single callback function, we can pass it as a ref to our elements. This ensures that all of our hooks work together seamlessly, eliminating potential issues from using multiple refs.
To better understand, here's an image to help visualize the process:
ts
const [draggableRef, dx, dy] = useDraggable();
const [resizableRef] = useResizable();
const ref = mergeRefs([draggableRef, resizableRef]);
This is the function that will merge different references for us:
ts
const mergeRefs = <T>(refs: Array<React.MutableRefObject<T> | React.LegacyRef<T> | null>): React.RefCallback<T> => {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current = value;
}
});
};
};
The `mergeRefs` function is super useful when you want to combine multiple refs into one callback ref that you can use with an element. It's a breeze to use: just pass in an array of refs and it'll give you back a new callback ref function.
Here's how it works: when you call the returned function, it loops through each ref in the array and checks its type. If it's a callback ref of type `function`, it calls it with the value passed to the callback ref. Otherwise, if it's not null, it sets its current value (by accessing the `current` property) to the passed value.
By merging different refs together using `mergeRefs`, you can create a single ref that lets you drag and resize elements with ease.
If you're interested in learning more, check out this post that covers how to merge different refs. Once you've got your merged ref, you can attach it to the cropping area and bam! You've got all the functionalities provided by the hooks above.
tsx
<div className="cropper__cropping" ref={ref}>
...
</div>

Saving the cropped image as a new file

We're almost done with our image cropper! The final step is to add the ability to save the cropped area as a new image. This feature is crucial because it allows users to use their customized images anywhere they want.
To make this possible, we'll use the HTML5 canvas element. The canvas element lets us draw graphics on a web page using JavaScript.
First, we need to create a new `canvas` element with the same dimensions as the cropping area. Then, we'll use the `drawImage()` method of the `canvas` context object to copy only the selected portion of our original image onto this new canvas.
tsx
const handleSaveImage = () => {
const img = originalImageRef.current;
const croppingArea = croppingAreaRef.current;

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

const scale = img.naturalWidth / img.width;
const croppingRect = croppingArea.getBoundingClientRect();
const scaledWidth = croppingRect.width * scale;
const scaledHeight = croppingRect.height * scale;
canvas.width = scaledWidth;
canvas.height = scaledHeight;

// Draw cropped image onto canvas
ctx.drawImage(img, dx * scale, dy * scale, scaledWidth, scaledHeight, 0, 0, scaledWidth, scaledHeight);

// Save image as file
const link = document.createElement("a");
link.href = canvas.toDataURL();
link.download = "cropped-image.png";

document.body.appendChild(link);
link.click();
link.remove();
};
In the code above, we create a new `canvas` element and set its size based on the area we want to crop. Then, we find our original image and calculate its size ratio by dividing its natural width by its displayed width.
Using the `drawImage()` method of our `canvas` context object, we copy only the selected part of our original image onto this new canvas. We pass it six arguments:
  • Our original image reference
  • The x and y coordinates of the upper-left corner of our cropping area multiplied by our scaling ratio
  • The width and height of our cropping area multiplied by the scaling ratio
  • The x and y coordinates to place the cropped image on the canvas
After drawing the cropped image onto our new canvas, we create a link element that points to the `dataURL` of this canvas. We also set its download attribute to `cropped-image.png` so that users can easily save it as a file.
Finally, we append this link element to our HTML document, simulate a `click` event on it, and remove it from the document. This downloads the cropped image as a PNG file.
With this functionality, users can select any part of an image they want and save it as a new file. That's it!

Demo

Check out this final demo of the steps we've been following so far.

Conclusion

To sum up, React is a fantastic tool to create an image cropper that gives users more control over their images. By combining the `useDraggable` and `useResizable` hooks, you can easily create a cropping area that can be customized to the user's needs.
We've shown how effortless it is to add these features to any element using the provided hooks. By merging different refs together, we can create a single ref that lets us drag and resize our elements with ease. We've also demonstrated how to use the HTML5 canvas element to save a cropped image as a new file.
With this knowledge, you can create your own custom image croppers quickly. The possibilities for customization options are limitless when it comes to your user's images!

See also

If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

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 🥷.
Ask me questions

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