Image Zoom Viewer

Create a component that allows users to hover over an image to view a magnified version of the part their cursor is over.

Solution

laptop on desk

Explanation

HTML Structure

You have a NextJS Image tag and a container to control it's size.

NextJS Image tag

By using the built in framework tag instead of an img tag, you get lots of benefits built in.

Handling image loading and source sets is handled automatically. However, there are set properties you are required to provide. Note on the tag the fill property.

Handling image size

NextJS will automatically add width and height of 100% so you need to control size and responsiveness with a container.

Looking at the .container styles, you can see the full width is taken and we need to provide a height.

Also the element has to have a position: relative so that the Image that has position: absolute knows which parent to position too. Otherwise it could use the body tag.

Creating the 'Zoom' effect

The principle behind this code is adding a zoom when the element is hovered over.

To achieve this we track the pointer position when the pointer is over the container. Note on the container div two functions: onMouseMove uses handleCursor and onMouseLeave will reset the tracked position with handleReset.

handleCursor

getBoundingClientRect returns positions of the offsets of the left and top part of the window.

event.clientX is providing the horizontal position of the mouse. To get it's real position, we need to remove the container's left offset from the window as well as the border width. The same is done in the vertical direction.

The position state is updated, and in turn used to update the position of the origin (where the centre of the object is) which aligns with the position of the mouse.

Code

import { MouseEvent, useMemo, useRef, useState } from "react";
import Image from "next/image";
import styles from "./Solution.module.css";

const Solution = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const initialPosition = useMemo(() => ({ x: "0", y: "0" }), []);
  const [position, setPosition] = useState(initialPosition);

  const handleCursor = (event: MouseEvent<HTMLDivElement>) => {
    if (containerRef.current) {
      // find offset of container in window
      const { left, top } = containerRef.current.getBoundingClientRect();
      // remove offset and border width on one side
      const x = event.clientX - left - 2;
      const y = event.clientY - top - 2;
      setPosition({ x: `${x}`, y: `${y}` });
    }
  };

  const handleReset = () => {
    setPosition(initialPosition);
  };

  return (
    <div
      ref={containerRef}
      onMouseMove={handleCursor}
      onMouseLeave={handleReset}
      className={styles.container}
    >
      <Image
        style={{
          transformOrigin: `${position.x}px ${position.y}px`,
          objectFit: "cover",
        }}
        className={styles.image}
        fill={true}
        priority={true}
        src="https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=1472&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
        alt="laptop on desk"
      />
    </div>
  );
};

export default Solution;

Styling

.container {
  position: relative;
  overflow: hidden;
  width: 100%;
  min-height: 350px;
  border: 2px solid black;
}

.image {
  /* absolute is added by next for Image */
  scale: 1;
  transition: 0.5s;
}

.container:hover .image {
  scale: 2.5;
}

Links