Build a Next.js Interactive Web-Based Image Resizer

Written by

in

In the digital age, images are everywhere. From social media to e-commerce, websites are filled with visual content. But what happens when you need to resize an image? Perhaps you want to optimize it for faster loading times, create thumbnails, or fit it into a specific layout. Manually resizing images can be tedious and time-consuming. Wouldn’t it be great to have a simple, interactive tool right in your browser to handle this task?

This tutorial will guide you through building a web-based image resizer using Next.js, a powerful React framework. We’ll create an application that allows users to upload an image, specify new dimensions, and instantly see the resized result. This project is perfect for beginners and intermediate developers looking to expand their skills with Next.js and image manipulation.

What You’ll Learn

By the end of this tutorial, you’ll be able to:

  • Set up a Next.js project.
  • Handle file uploads in a Next.js application.
  • Use JavaScript’s built-in FileReader API to preview images.
  • Implement image resizing logic using the canvas element.
  • Create a user-friendly interface with React components.
  • Understand basic image optimization principles.

Prerequisites

Before we begin, make sure you have the following:

  • Node.js and npm (or yarn) installed on your system.
  • A basic understanding of HTML, CSS, and JavaScript.
  • Familiarity with React concepts like components, state, and props (though the tutorial will explain the key parts).
  • A text editor or IDE (like VS Code) to write your code.

Step 1: Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app image-resizer
cd image-resizer

This command creates a new Next.js project named “image-resizer” and navigates you into the project directory. Next.js will handle all the initial setup, including installing the necessary dependencies.

Step 2: Project Structure and File Overview

After the project is created, your directory structure should look something like this:


image-resizer/
├── node_modules/
├── pages/
│   └── _app.js
│   └── index.js
├── public/
├── .gitignore
├── package.json
├── README.md
└── next.config.js

Let’s briefly go over the important files:

  • pages/index.js: This is the main page of our application. We’ll write the core logic and UI here.
  • public/: This directory is for static assets like images, fonts, and other files that are directly served to the browser.
  • package.json: This file contains the project’s dependencies and scripts.

Step 3: Creating the User Interface (UI)

Open pages/index.js and replace the default content with the following code. This sets up the basic layout, including an image upload input, input fields for width and height, and an area to display the resized image.

import { useState } from 'react';

export default function Home() {
  const [image, setImage] = useState(null);
  const [width, setWidth] = useState('');
  const [height, setHeight] = useState('');
  const [resizedImage, setResizedImage] = useState(null);

  const handleImageChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (event) => {
        setImage(event.target.result);
      };
      reader.readAsDataURL(file);
    }
  };

  const handleWidthChange = (e) => {
    setWidth(e.target.value);
  };

  const handleHeightChange = (e) => {
    setHeight(e.target.value);
  };

  const handleResize = () => {
    // Resize logic will go here
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Image Resizer</h2>
      <input type="file" accept="image/*" onChange={handleImageChange} />
      <br />
      <label htmlFor="width">Width:</label>
      <input type="number" id="width" value={width} onChange={handleWidthChange} />
      <br />
      <label htmlFor="height">Height:</label>
      <input type="number" id="height" value={height} onChange={handleHeightChange} />
      <br />
      <button onClick={handleResize}>Resize</button>
      <br />
      {image && <img src={image} alt="Uploaded Image" style={{ maxWidth: '100%', marginTop: '10px' }} />}
      {resizedImage && <img src={resizedImage} alt="Resized Image" style={{ maxWidth: '100%', marginTop: '10px' }} />}
    </div>
  );
}

Let’s break down this code:

  • We import the useState hook from React to manage the state of our component. We’ll use this to store the uploaded image, the desired width and height, and the resized image.
  • We define several state variables: image (the original image URL), width (the desired width), height (the desired height), and resizedImage (the resized image URL).
  • handleImageChange: This function is triggered when the user selects an image. It uses the FileReader API to read the image file and convert it to a data URL, which can be displayed in an <img> tag.
  • handleWidthChange and handleHeightChange: These functions update the width and height state variables when the user types in the input fields.
  • handleResize: This function will contain the image resizing logic (we’ll implement this in the next step).
  • The JSX (HTML-like syntax) displays the UI elements: an image upload input, input fields for width and height, a resize button, and areas to display the original and resized images.

Step 4: Implementing Image Resizing Logic

Now, let’s add the image resizing functionality to the handleResize function. This is where the magic happens. We’ll use the canvas element to resize the image.


const handleResize = () => {
  if (!image || !width || !height) {
    alert('Please upload an image and enter width and height.');
    return;
  }

  const img = new Image();
  img.onload = () => {
    const canvas = document.createElement('canvas');
    canvas.width = parseInt(width, 10);
    canvas.height = parseInt(height, 10);
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    const resizedImageDataURL = canvas.toDataURL('image/jpeg'); // Or 'image/png'
    setResizedImage(resizedImageDataURL);
  };
  img.src = image;
};

Let’s walk through this code:

  • First, we check if an image has been uploaded and if width and height have been entered. If not, we show an alert.
  • We create a new Image object.
  • img.onload: This is an event handler that runs when the image has finished loading. Inside this handler, we perform the resizing.
  • We create a canvas element. The canvas is an HTML element that can be used to draw graphics.
  • We set the canvas width and height to the values entered by the user (parsed as integers).
  • We get the 2D rendering context of the canvas using canvas.getContext('2d'). This context provides methods for drawing on the canvas.
  • ctx.drawImage(img, 0, 0, canvas.width, canvas.height): This is the core of the resizing. It draws the original image onto the canvas, scaling it to the specified width and height. The first two arguments (0, 0) specify the starting x and y coordinates on the canvas.
  • canvas.toDataURL('image/jpeg'): This method converts the canvas content to a data URL, which is a string representation of the image. We specify ‘image/jpeg’ as the format, but you can also use ‘image/png’ if you prefer.
  • Finally, we update the resizedImage state with the data URL of the resized image, triggering a re-render and displaying the resized image on the page.
  • img.src = image;: This sets the source of the image to the uploaded image data URL, which triggers the onload event.

Step 5: Running the Application

To run your application, open your terminal, navigate to the project directory (image-resizer), and run the following command:

npm run dev
# or
yarn dev

This will start the development server. Open your web browser and go to http://localhost:3000. You should see your image resizer application. Upload an image, enter the desired width and height, and click the “Resize” button. The resized image should appear below.

Step 6: Adding Error Handling and Input Validation

To make our application more robust, let’s add some error handling and input validation. This will prevent unexpected behavior and provide a better user experience.

First, let’s add validation to ensure that the width and height inputs are valid numbers and are not negative or zero. Modify the handleResize function as follows:


const handleResize = () => {
  if (!image || !width || !height) {
    alert('Please upload an image and enter width and height.');
    return;
  }

  const parsedWidth = parseInt(width, 10);
  const parsedHeight = parseInt(height, 10);

  if (isNaN(parsedWidth) || parsedWidth <= 0 || isNaN(parsedHeight) || parsedHeight  {
    const canvas = document.createElement('canvas');
    canvas.width = parsedWidth;
    canvas.height = parsedHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    const resizedImageDataURL = canvas.toDataURL('image/jpeg'); // Or 'image/png'
    setResizedImage(resizedImageDataURL);
  };
  img.src = image;
};

We’ve added the following checks:

  • parseInt(width, 10) and parseInt(height, 10): We parse the width and height strings into integers.
  • isNaN(parsedWidth) || parsedWidth <= 0 || isNaN(parsedHeight) || parsedHeight <= 0: We check if the parsed values are not a number (isNaN) or if they are less than or equal to zero. If any of these conditions are true, we display an error message.

Next, let’s add error handling for the image loading process. What if the image fails to load for some reason? We can use a try...catch block to handle potential errors.


const handleResize = () => {
  if (!image || !width || !height) {
    alert('Please upload an image and enter width and height.');
    return;
  }

  const parsedWidth = parseInt(width, 10);
  const parsedHeight = parseInt(height, 10);

  if (isNaN(parsedWidth) || parsedWidth <= 0 || isNaN(parsedHeight) || parsedHeight  {
    const canvas = document.createElement('canvas');
    canvas.width = parsedWidth;
    canvas.height = parsedHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    const resizedImageDataURL = canvas.toDataURL('image/jpeg'); // Or 'image/png'
    setResizedImage(resizedImageDataURL);
  };
  img.onerror = () => {
    alert('Error loading image. Please try again.');
  };
  img.src = image;
};

We’ve added an img.onerror handler. This function will execute if the image fails to load. This provides feedback to the user and prevents the application from crashing.

Step 7: Optimizing the User Experience

Let’s make a few improvements to enhance the user experience:

  • Clear Input Fields After Resize: After the image is resized, it’s good practice to clear the width and height input fields. This gives the user a visual cue that the operation is complete and prepares the form for the next resize.
  • Add a Loading Indicator: While the image is resizing (which might take a moment, depending on the image size and the user’s internet connection), it’s helpful to show a loading indicator. This lets the user know that the application is working and prevents them from clicking the resize button multiple times.

Let’s implement these improvements. Modify the handleResize function as follows:


import { useState } from 'react';

export default function Home() {
  const [image, setImage] = useState(null);
  const [width, setWidth] = useState('');
  const [height, setHeight] = useState('');
  const [resizedImage, setResizedImage] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleImageChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (event) => {
        setImage(event.target.result);
      };
      reader.readAsDataURL(file);
    }
  };

  const handleWidthChange = (e) => {
    setWidth(e.target.value);
  };

  const handleHeightChange = (e) => {
    setHeight(e.target.value);
  };

  const handleResize = () => {
    if (!image || !width || !height) {
      alert('Please upload an image and enter width and height.');
      return;
    }

    const parsedWidth = parseInt(width, 10);
    const parsedHeight = parseInt(height, 10);

    if (isNaN(parsedWidth) || parsedWidth <= 0 || isNaN(parsedHeight) || parsedHeight  {
      const canvas = document.createElement('canvas');
      canvas.width = parsedWidth;
      canvas.height = parsedHeight;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      const resizedImageDataURL = canvas.toDataURL('image/jpeg'); // Or 'image/png'
      setResizedImage(resizedImageDataURL);
      setLoading(false);
      setWidth('');
      setHeight('');
    };
    img.onerror = () => {
      alert('Error loading image. Please try again.');
      setLoading(false);
    };
    img.src = image;
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Image Resizer</h2>
      <input type="file" accept="image/*" onChange={handleImageChange} />
      <br />
      <label htmlFor="width">Width:</label>
      <input type="number" id="width" value={width} onChange={handleWidthChange} />
      <br />
      <label htmlFor="height">Height:</label>
      <input type="number" id="height" value={height} onChange={handleHeightChange} />
      <br />
      <button onClick={handleResize} disabled={loading}>{loading ? 'Resizing...' : 'Resize'}</button>
      <br />
      {image && <img src={image} alt="Uploaded Image" style={{ maxWidth: '100%', marginTop: '10px' }} />}
      {resizedImage && <img src={resizedImage} alt="Resized Image" style={{ maxWidth: '100%', marginTop: '10px' }} />}
    </div>
  );
}

Here’s what changed:

  • We added a loading state variable, initialized to false.
  • Inside handleResize, we set setLoading(true) at the beginning, before the image loading.
  • In the img.onload and img.onerror handlers, we set setLoading(false) to indicate that the resizing process is complete (or has failed).
  • Inside img.onload, after setting the resizedImage, we reset the width and height input values to an empty string (setWidth('') and setHeight('')).
  • We disabled the “Resize” button while loading is true (disabled={loading}). We also change the button’s text to “Resizing…” while loading.

Step 8: Adding Image Optimization (Optional)

While our application resizes images, we can also explore ways to *optimize* the resized images. Image optimization is crucial for improving website performance and reducing loading times. Here are a few things to consider:

  • Choosing the Right Image Format: The canvas.toDataURL() method allows you to specify the image format. We’re using ‘image/jpeg’ in this example, which is generally suitable for photographs. However, for images with sharp lines and text, ‘image/png’ might be a better choice as it supports lossless compression (meaning no quality is lost). Consider allowing the user to select the output format.
  • Compression: For JPEG images, you can control the compression quality. The toDataURL() method accepts a second argument, the quality, which is a number between 0 and 1. A lower value means more compression (smaller file size) but potentially lower image quality.
  • Lazy Loading: For images that are not immediately visible on the screen (e.g., images further down the page), consider using lazy loading. This means the image is only loaded when it’s about to become visible. This can significantly improve the initial page load time. Next.js provides built-in support for image optimization and lazy loading via the next/image component.

Here’s an example of how you might adjust the quality of the JPEG compression:


const resizedImageDataURL = canvas.toDataURL('image/jpeg', 0.8); // Adjust quality (0.0 - 1.0)

In this example, we’ve set the quality to 0.8 (80%). Experiment with different quality levels to find the right balance between file size and image quality for your use case. Remember that image optimization is often a trade-off between file size and visual quality.

Step 9: Deploying Your Application

Once you’re satisfied with your image resizer, you’ll want to deploy it so others can use it. Next.js offers several deployment options. Here’s how you can deploy your application to Vercel, which is a popular and easy-to-use platform for Next.js applications:

  1. Create a Vercel Account: If you don’t already have one, sign up for a free Vercel account at vercel.com.
  2. Connect Your Git Repository: Vercel integrates seamlessly with Git repositories (like GitHub, GitLab, or Bitbucket). Push your code to your repository.
  3. Import Your Project: In your Vercel dashboard, click “Add New Project.” Select your Git repository.
  4. Configure Deployment (If Necessary): Vercel will usually detect that it’s a Next.js project and configure the deployment automatically. You can review the settings and make any necessary adjustments.
  5. Deploy: Click “Deploy.” Vercel will build and deploy your application.
  6. Access Your Application: Once the deployment is complete, Vercel will provide you with a unique URL where your application is live.

Vercel handles the build process, server setup, and other deployment details, making it incredibly easy to get your Next.js application online.

Key Takeaways

  • You’ve learned how to build an interactive image resizer using Next.js.
  • You’ve gained experience with file uploads, the FileReader API, and the canvas element.
  • You’ve implemented image resizing logic and created a user-friendly interface.
  • You’ve learned how to add error handling and input validation to improve the robustness of your application.
  • You’ve explored image optimization techniques to improve performance.

FAQ

  1. Can I use this image resizer for commercial purposes? Yes, you can use the code from this tutorial for both personal and commercial projects.
  2. How can I add more image formats (e.g., WebP)? You would need to use a library that supports WebP encoding (like sharp) and integrate it with your Next.js application. This is a more advanced topic.
  3. How can I allow users to download the resized image? You can add a download button that uses the resizedImage data URL as the href attribute and sets the download attribute to specify the filename.
  4. Why is the image quality sometimes lower after resizing? Image quality can be affected by the resizing algorithm used by the browser, and by the compression settings (e.g., the quality parameter when creating the data URL). Experiment with different settings to find the best balance. Consider using a dedicated image processing library for more control.

Building this image resizer is a great way to deepen your understanding of Next.js and frontend development. You’ve learned how to handle file uploads, manipulate images, and create a user-friendly application. This project can be extended further: you could add features like cropping, rotation, or more advanced image editing options. The skills you’ve gained can be applied to many other web development projects. Continue to experiment, explore new libraries, and iterate on your skills. The world of web development is constantly evolving, and there’s always something new to learn and build. By creating this image resizer, you’ve taken a significant step in your journey as a developer, and you can now confidently tackle more complex projects.