React image zoom

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

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 as filename.module.css

React image zoom folder structure

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;