Learn how to create image zoom functionality in react.
When you visit a e-commerce site you often see that you can expand and view the product image.
Image zoomed viewer is used to display image in expanded view when user hovers on the image, sometimes on click as well.
Something like this.
React image zoom component.
We are going to build a component in react which will zoom the image on user click as well as on hover and touch.
For our development we will require few extra packages.
proptypes
: To validate the props we will recieve.classnames
: This helps us to use CSS classes as javascript objects, we just need to name our CSS file asfilename.module.css
Let us start the development by importing all the required packages and setting up things.
import React, { useRef } from "react"; import PropTypes from "prop-types"; import cx from "classnames"; import styles from "./index.module.css"; const ImageZoom = props => { //Other code will go here } export default ImageZoom;
This is a functional component because we want the parent to control it. It will accept the normal and zoomed image URL’s and show the zoomed images based on the user action provided.
We are also using react hook useRef
because we need to create ref to the images in the functional component.
Validate the props before moving any forward. This helps to get a good idea of all the feature that we need in our component.
ImageZoom.propTypes = { imageURL: PropTypes.string.isRequired, zoomImageURL: PropTypes.string.isRequired, placement: PropTypes.oneOf([ "top-left", "top-right", "bottom-left", "bottom-right", "center" ]).isRequired, imageSize: PropTypes.shape({ width: PropTypes.number.isRequired, height: PropTypes.number.isRequired }), zoomedImageSize: PropTypes.shape({ width: PropTypes.number.isRequired, height: PropTypes.number.isRequired }), isActive: PropTypes.bool.isRequired, onZoom: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, zoomType: PropTypes.oneOf(["click", "hover"]).isRequired }; ImageZoom.defaultProps = { zoomImageURL: "", placement: "top-right", imageSize: { width: 300, height: 300 }, zoomedImageSize: { width: 600, height: 600 }, isActive: false, zoomType: "hover" };
The props are extremely simple and generic to create an functional image zoom viewer. We are providing the zoom action on two different events click
and hover
.
Default props are also set in case user misses the essential props.
Now as we have good idea about what we have to build lets start the development.
First, create the layout of the component, this way we will get a good picture of what all methods we need.
return ( <> {/* Actual Image */} <div className={cx(styles.normalImage, { [styles.zoomOutCursor]: isActive })} style={normalImageStyle} ref={normalImageRef} {...eventType} > {/* Zoomed Image View Area */} {isActive && ( <div className={cx(styles.zoomedImage, styles[placement])} style={zoomedImageStyle} ref={zoomedImageRef} ></div> )} </div> </> );
We have placed the zoomed image viewer inside the normal image because we want it to be relative to the parent so that we can decide its placement.
Also the image will be set as background images as it provides great control over the aspect ratio of the image.
Assign all the props and create the style of each element.
const { imageURL, zoomImageURL, placement, imageSize, zoomedImageSize, isActive, zoomType } = props; let normalImageRef = useRef(); let zoomedImageRef = useRef(); //Set the style of normal image const normalImageStyle = { backgroundImage: `url(${imageURL})`, backgroundSize: `${imageSize.width}px ${imageSize.height}px`, width: `${imageSize.width}px`, height: `${imageSize.height}px` }; //Set the style of zoomed image const zoomedImageStyle = { backgroundImage: `url(${zoomImageURL || imageURL})`, backgroundSize: zoomType === "click" ? `${zoomedImageSize.width}px ${zoomedImageSize.height}px` : `${zoomedImageSize.width * 1.5}px ${zoomedImageSize.height * 1.5}px`, backgroundRepeat: "no-repeat", width: `${zoomedImageSize.width}px`, height: `${zoomedImageSize.height}px` };
Different style will be assigned to the elements, we have kept the zoom area bigger 1.5x
purposely.
Now lets assign the different events based on the user action type received.
//Set the events based on the type const eventType = zoomType === "click" ? { onClick: isActive ? closeZoom : openZoom } : { onMouseMove: openZoom, onMouseLeave: closeZoom, onTouchMove: openZoom, onTouchEnd: closeZoom, onTouchCancel: closeZoom }; //Show image const openZoom = e => { if (zoomedImageRef.current) { //Focus on hovered area moveLens(e); } const { onZoom } = props; onZoom && onZoom(); }; //Hide image const closeZoom = () => { const { onClose } = props; onClose && onClose(); };
For the click
event simple toggle will work, as it will only show and hide zoomed image.
But for hover
there is a lot of work undergoes because we need to show the zoomed part of the focused part on the normal image. Where our cursor is residing currently.
To achieve this we need to calculate the cursor position relative to the normal image and the use it to show the current position on zoomed image.
//Get cursor position const getCursorPos = e => { let a, x = 0, y = 0; e = e || window.event; /* Get the x and y positions of the image: */ a = normalImageRef.current.getBoundingClientRect(); /* Calculate the cursor's x and y coordinates, relative to the image: */ x = e.pageX - a.left; y = e.pageY - a.top; /* Consider any page scrolling: */ x = x - window.pageXOffset; y = y - window.pageYOffset; return { x: x, y: y }; }; //Focus over the zommed image const moveLens = e => { const viewArea = zoomedImageRef.current; /* Prevent any other actions that may occur when moving over the image */ e.preventDefault(); /* Get the cursor's x and y positions: */ const { x, y } = getCursorPos(e); //Move the zoomed image viewArea.style.backgroundPosition = `-${x}px -${y}px`; };
As we have already created the refs for both our images we can use it to directly update their styles instead of updating the state.
Complete code of zoomed image viewer in react.
import React, { useRef } from "react"; import PropTypes from "prop-types"; import cx from "classnames"; import styles from "./index.module.css"; const ImageZoom = props => { const { imageURL, zoomImageURL, placement, imageSize, zoomedImageSize, isActive, zoomType } = props; let normalImageRef = useRef(); let zoomedImageRef = useRef(); //Set the style of normal image const normalImageStyle = { backgroundImage: `url(${imageURL})`, backgroundSize: `${imageSize.width}px ${imageSize.height}px`, width: `${imageSize.width}px`, height: `${imageSize.height}px` }; //Set the style of zoomed image const zoomedImageStyle = { backgroundImage: `url(${zoomImageURL || imageURL})`, backgroundSize: zoomType === "click" ? `${zoomedImageSize.width}px ${zoomedImageSize.height}px` : `${zoomedImageSize.width * 1.5}px ${zoomedImageSize.height * 1.5}px`, backgroundRepeat: "no-repeat", width: `${zoomedImageSize.width}px`, height: `${zoomedImageSize.height}px` }; //Set the events based on the type const eventType = zoomType === "click" ? { onClick: isActive ? closeZoom : openZoom } : { onMouseMove: openZoom, onMouseLeave: closeZoom, onTouchMove: openZoom, onTouchEnd: closeZoom, onTouchCancel: closeZoom }; //Show image const openZoom = e => { if (zoomedImageRef.current) { moveLens(e); } const { onZoom } = props; onZoom && onZoom(); }; //Hide image const closeZoom = () => { const { onClose } = props; onClose && onClose(); }; //Get cursor position const getCursorPos = e => { let a, x = 0, y = 0; e = e || window.event; /* Get the x and y positions of the image: */ a = normalImageRef.current.getBoundingClientRect(); /* Calculate the cursor's x and y coordinates, relative to the image: */ x = e.pageX - a.left; y = e.pageY - a.top; /* Consider any page scrolling: */ x = x - window.pageXOffset; y = y - window.pageYOffset; return { x: x, y: y }; }; //Focus over the zommed image const moveLens = e => { const viewArea = zoomedImageRef.current; /* Prevent any other actions that may occur when moving over the image */ e.preventDefault(); /* Get the cursor's x and y positions: */ const { x, y } = getCursorPos(e); //Move the zoomed image viewArea.style.backgroundPosition = `-${x}px -${y}px`; }; return ( <> {/* Actual Image */} <div className={cx(styles.normalImage, { [styles.zoomOutCursor]: isActive })} style={normalImageStyle} ref={normalImageRef} {...eventType} > {/* Zoomed Image View Area */} {isActive && ( <div className={cx(styles.zoomedImage, styles[placement])} style={zoomedImageStyle} ref={zoomedImageRef} ></div> )} </div> </> ); }; ImageZoom.propTypes = { imageURL: PropTypes.string.isRequired, zoomImageURL: PropTypes.string.isRequired, placement: PropTypes.oneOf([ "top-left", "top-right", "bottom-left", "bottom-right", "center" ]).isRequired, imageSize: PropTypes.shape({ width: PropTypes.number.isRequired, height: PropTypes.number.isRequired }), zoomedImageSize: PropTypes.shape({ width: PropTypes.number.isRequired, height: PropTypes.number.isRequired }), isActive: PropTypes.bool.isRequired, onZoom: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, zoomType: PropTypes.oneOf(["click", "hover"]).isRequired }; ImageZoom.defaultProps = { zoomImageURL: "", placement: "top-right", imageSize: { width: 300, height: 300 }, zoomedImageSize: { width: 600, height: 600 }, isActive: false, zoomType: "hover" }; export default ImageZoom;
Complete style code of image viewer
As we have given option to decide the placement of the zoomed image, we have to write the CSS code for it.
The normal image is relative and the zoomed image is contained inside it which will be absolute to it.
.normalImage { cursor: zoom-in; position: relative; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); } .zoomedImage { position: absolute; z-index: 999; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); } .zoomOutCursor { cursor: zoom-out; } .center { top: 50%; left: 50%; transform: translate(-50%, -50%); } .top-left { top: 0; left: -110%; } .top-right { top: 0; left: 110%; } .bottm-left { bottom: 0; left: -110%; } .bottom-right { bottom: 0; left: 110%; }
Input
import React, { Component } from "react"; import ImageZoom from "./index"; class ImageZoomTest extends Component { state = { isActive: false }; onClose = () => { this.setState({ isActive: false }); }; onZoom = () => { this.setState({ isActive: true }); }; render() { const { isActive } = this.state; return ( <ImageZoom isActive={isActive} imageURL={ "https://cdn.pixabay.com/photo/2019/12/30/13/10/lost-places-4729640_1280.jpg" } onZoom={this.onZoom} onClose={this.onClose} /> ); } } export default ImageZoomTest;