Building an Image Magnifier Effect in NEXT JS: A Step-by-Step Guide

thumbnail

Hello Guys, In this tutorial, we'll build a custom Image Magnifier component using Next JS. Let’s dive into the step-by-step guide to creating your own Image Magnifier component.

Prerequisites:

Before starting, make sure you have a Next.js project set up. If not, you can create one using the following commands:

npx create-next-app@latest

You can place one sample image inside the public folder. Now let’s break down the component step by step:

Imports & Constants:

import React, { MouseEvent, useEffect, useState } from 'react';
import sampleImg from '../../public/sample.png';
import Image from 'next/image';

const MAGNIFIER_SIZE = 100;
const ZOOM_LEVEL = 2.5;

Here, necessary components and hooks are imported, along with the sample image and Next.js Image component. Constants define the size of the magnifier and the level of zoom to be applied.

State Variables

..
const ImageEffect = () => {
  // State variables
  const [zoomable, setZoomable] = useState(false);
  const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
  const [position, setPosition] = useState({ x: 0, y: 0, mouseX: 0, mouseY: 0 });

This part defines the state variables using the useState hook. zoomable tracks whether the magnifier is active(later magnifier will activated or deactivated if cursor is on image or not), imageSize stores the dimensions of the main image, and position maintains the coordinates for the magnifier.

Event Handlers:

...
// Event handlers
  const handleMouseEnter = (e: MouseEvent) => {
    let element = e.currentTarget;
    let { width, height } = element.getBoundingClientRect();
    setImageSize({ width, height });
    setZoomable(true);
    updatePosition(e);
  };
  const handleMouseLeave = (e: MouseEvent) => {
    setZoomable(false);
    updatePosition(e);
  };
  const handleMouseMove = (e: MouseEvent) => {
    updatePosition(e);
  };
  const updatePosition = (e: MouseEvent) => {
    const { left, top } = e.currentTarget.getBoundingClientRect();
    let x = e.clientX - left;
    let y = e.clientY - top;
    setPosition({
      x: -x * ZOOM_LEVEL + (MAGNIFIER_SIZE / 2),
      y: -y * ZOOM_LEVEL + (MAGNIFIER_SIZE / 2),
      mouseX: x - (MAGNIFIER_SIZE / 2),
      mouseY: y - (MAGNIFIER_SIZE / 2),
    });
  };

These functions handle mouse enter, leave, and move events. handleMouseEnter extracts the dimensions of the main image with the help of current target which provides the element we are in and getting the width and height of that element after that we set the width and height to the state and, set zoomable to true, and calls updatePosition. handleMouseLeave sets zoomable to false, and handleMouseMove also calls updatePosition. Now in updatePosition function we get the left(x) and top(y) value of the image / element, next we subtract e.clientX(current mouse x position) with the left value so that our x value will be clamped within the image area, similarly we calculate for y. Now will be setting position to state, for the magnifier box we calculate the values for mouseX and mouseY by subtracting x with half of magnifier size such that the box follows our cursor and it is aligned to center. Now for that zooming effect, we will use x, y values which will be then passed to background position css style, for x we need to multiple with the zoom level and add it to the half magnifier size for adjustments, similarly calculate for y.

JSX:

return (
    <div className='flex justify-center items-center'>
      <div
        onMouseLeave={handleMouseLeave}
        onMouseEnter={handleMouseEnter}
        onMouseMove={handleMouseMove}
        className='w-80 h-96 relative overflow-hidden'>
        <Image
          className='object-cover border z-10'
          alt=""
          src={sampleImg}
          fill
        />
        <div
          style={{
            backgroundPosition: `${position.x}px ${position.y}px`,
            backgroundImage: `url(${sampleImg.src})`,
            backgroundSize: `${imageSize.width * ZOOM_LEVEL}px ${imageSize.height * ZOOM_LEVEL}px`,
            backgroundRepeat: 'no-repeat',
            display: zoomable ? 'block' : 'none',
            top: `${position.mouseY}px`,
            left: `${position.mouseX}px`,
            width: `${MAGNIFIER_SIZE}px`,
            height: `${MAGNIFIER_SIZE}px`,
          }}
          className={`z-50 border-4 rounded-full pointer-events-none absolute border-gray-500`}
        />
      </div>
    </div>
  );
};

This outer div creates a with flex layout, centering its content both horizontally and vertically. This inner div is the container for the image and magnifier. It has a fixed width of 80 and height of 96 with tailwindcss. The relative class is applied to allow child elements to be positioned relative to this container. The overflow-hidden class ensures that any overflowing content is hidden. The Image component is used from the next/image library. It fills its container (object-cover class) and has a border with a z-10 z-index. The fill prop ensures the image takes up the entire container. Finally the last div represents the magnifier. Its styling is dynamically controlled by state variables and mouse events. Let's break down the style properties:

  • backgroundPosition: Determines the position of the background image within the magnifier , value will be populated from position state object x and y properties.
  • backgroundImage: Sets the background image to the main image.
  • backgroundSize: Adjusts the size of the background image based on the zoom level and width & height of the image which is set in imageSize state variable.
  • backgroundRepeat: Ensures the background image doesn't repeat.
  • display: Controls whether the magnifier is visible (block) or hidden (none) based on the zoomable state.
  • top and left: Position the magnifier based on the mouse coordinates from the position object property mouseY and mouseX.
  • width and height: Set the dimensions of the magnifier.
  • The className includes styling for the magnifier, such as a border, rounded corners for making it as circle, and positioning.

Final Code for the component.

// Import dependencies
import React, { MouseEvent, useEffect, useState } from 'react';
import sampleImg from '../../public/sample.png';
import Image from 'next/image';

// Constants for magnifier size and zoom level
const MAGNIFIER_SIZE = 100;
const ZOOM_LEVEL = 2.5;

// ImageEffect component
const ImageEffect = () => {
    // State variables
    const [zoomable, setZoomable] = useState(true);
    const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
    const [position, setPosition] = useState({ x: 100, y: 100, mouseX: 0, mouseY: 0 });

    // Event handlers
    const handleMouseEnter = (e: MouseEvent) => {
        let element = e.currentTarget;
        let { width, height } = element.getBoundingClientRect();
        setImageSize({ width, height });
        setZoomable(true);
        updatePosition(e);
    };

    const handleMouseLeave = (e: MouseEvent) => {
        setZoomable(false);
        updatePosition(e);
    };

    const handleMouseMove = (e: MouseEvent) => {
        updatePosition(e);
    };

    const updatePosition = (e: MouseEvent) => {
        const { left, top } = e.currentTarget.getBoundingClientRect();
        let x = e.clientX - left;
        let y = e.clientY - top;
        setPosition({
            x: -x * ZOOM_LEVEL + (MAGNIFIER_SIZE / 2),
            y: -y * ZOOM_LEVEL + (MAGNIFIER_SIZE / 2),
            mouseX: x - (MAGNIFIER_SIZE / 2),
            mouseY: y - (MAGNIFIER_SIZE / 2),
        });
    };

    // Render method
    return (
        <div className='flex justify-center items-center'>
            <div
                onMouseLeave={handleMouseLeave}
                onMouseEnter={handleMouseEnter}
                onMouseMove={handleMouseMove}
                className='w-80 h-96 relative overflow-hidden'>
                <Image className='object-cover border z-10' alt="" src={sampleImg} fill />
                <div
                    style={{
                        backgroundPosition: `${position.x}px ${position.y}px`,
                        backgroundImage: `url(${sampleImg.src})`,
                        backgroundSize: `${imageSize.width * ZOOM_LEVEL}px ${imageSize.height * ZOOM_LEVEL}px`,
                        backgroundRepeat: 'no-repeat',
                        display: zoomable ? 'block' : 'none',
                        top: `${position.mouseY}px`,
                        left: `${position.mouseX}px`,
                        width: `${MAGNIFIER_SIZE}px`,
                        height: `${MAGNIFIER_SIZE}px`,
                    }}
                    className={`z-50 border-4 rounded-full pointer-events-none absolute border-gray-500`}
                />
            </div>
        </div>
    );
};

export default ImageEffect;

Finally, you can add this component to your page and run the NEXT JS app with the command npm run dev .

import ImageEffect from '@/components/image-effect'

export default function Home() {
  return (
    <main className="min-h-screen flex flex-col text-center justify-center items-center">
      <h1 className='text-5xl mb-10 font-bold'>IMAGE MAGNIFIER</h1>
      <ImageEffect />
    </main>
  )
}

Congratulations! You’ve successfully built an Image Magnifier component with Next.js. Feel free to customize the styles and add additional functionalities to suit your project’s needs.