Change a Background with Cloudinary & Next.js

Colby Fayock

When dealing with images, often you want to be able to cut out the primary object and change the background, whether that's a pair of shoes you want to show in your store in a more life-like environment or if you simply want to change the background.

But doing this manually can be a huge pain and take a lot of time!

Instead, we can take advantage of Cloudinary AI using it's background removal tool to cut out our images and swap in whatever background we'd like!

This is what our main goal will be to achieve.

See Code on GitHub

Step 0: Setting up a workspace

In my example, I'm using Next.js.

This allows me to set up API endpoints that use serverless functions to proxy my custom uploads to Cloudinary.

Whether following along with Next.js or not, the code we'll be using should be transferrable to any other node.js-based application.

To get started, I used the this Next.js Starter that I created that creates a basic UI that will allow us to upload an image.

You can do the same by running the command:

1yarn create next-app -e https://github.com/colbyfayock/demo-image-upload-starter my-image-background
2# or
3npx create-next-app -e https://github.com/colbyfayock/demo-image-upload-starter my-image-background

You can then run the project locally by running:

1yarn dev
2# or
3npm run dev

Where we can see that we have a file-picker and when selecting an image, we present that image on the page!

Step 1: Installing and configuring the Cloudinary Node SDK

We'll be using the Cloudinary Node SDK in order to upload our images and make the available to change the background.

First, let's install the SDK with:

1yarn add cloudinary
2# or
3npm install cloudinary

Next, we want to import v2 of the SDK into our project with:

1import { v2 as cloudinary } from 'cloudinary';
2# or
3const cloudinary = require('cloudinary').v2;

Finally, to configure the SDK, we'll need to configure out Cloudinary Cloud Name, API Key, and API Secret.

I recommend doing this by setting up a local environment variables file to avoid committing your secret keys to your Git provider or generally storing sensitive data.

First, create a file in the root of your project called .env.local and add the following:

1NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="<Your Cloud Name>"
2CLOUDINARY_API_KEY="<Your API Key>"
3CLOUDINARY_API_SECRET="<Your API Secret>"

You can find these values right at the top of your Cloudinary dashboard.

Note: we're prefixing the Cloud Name with NEXT_PUBLIC so that we can use that value in the client. Technically we don't need to store this value in an environment variable, but since we're storing everything else, it makes it easier to manage from a single location.

Now when we go to use our SDK, we'll have those values available.

Follow along with the commit!

Step 2: Proxying secure Upload and Resource requests with Next.js serverless functions

In order to upload as a signed request and fetch our resource, which we'll need later, we need to be able to use our credentials that we set up inside of .env.local.

We'll use Next.js serverless functions which give us a node environment where those values won't be expose, but still useable.

Navigating to the pages/api directory, we can first add our Upload endpoint by creating a file called upload.js.

Inside pages/api/upload.js add:

1const cloudinary = require('cloudinary').v2;
2
3cloudinary.config({
4 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
5 api_key: process.env.CLOUDINARY_API_KEY,
6 api_secret: process.env.CLOUDINARY_API_SECRET,
7 secure: true
8});
9
10export default async function handler(req, res) {
11 const { image, options = {} } = JSON.parse(req.body);
12
13 const results = await cloudinary.uploader.upload(image, options);
14
15 res.status(200).json(results);
16}

Here we're:

  • Importing the Cloudinary SDK
  • Configuring the Cloudinary SDK with our credentials
  • Creating a new serverless function handler
  • Parsing an image and options value from the request's body
  • Passing those values into the Cloudinary uploader
  • Returning a 200 response with those results

This will make a new endpoint available at /api/upload that we'll be able to POST our uploads to.

Next, we'll create another similar function for grabbing the details of a resource (like an image).

Create a file called resource.js inside of pages/api and inside of pages/api/resource.js add:

1const cloudinary = require('cloudinary').v2;
2
3cloudinary.config({
4 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
5 api_key: process.env.CLOUDINARY_API_KEY,
6 api_secret: process.env.CLOUDINARY_API_SECRET,
7 secure: true,
8});
9
10export default async function handler(req, res) {
11 const { publicId } = req.query || {};
12
13 const results = await cloudinary.api.resource(publicId);
14
15 res.status(200).json(results);
16}

Here we're:

  • Importing and configuring Cloudinary
  • Creating a new function where this time we grab a different data value from the query parameters
  • Passing that to the Cloudinary resource method
  • Returning the response

Similar to our Upload endpoint, this will make a GET request available at /api/resource?publicId=<Public ID>.

Note: there's not really a distinct difference between POST and GET requests when creating the functions beyond how you accept the data where with POST we were using the body and GET we're using query parameters.

Now that our endpoints are both set up, we'll be ready to get to work managing these assets in our app!

Follow along with the commit!

Step 3: Uploading an form image to Cloudinary

In the app that we set up, we have available to use a form with a file-picker that will store our image in our local app's state.

If you're following along, inside of our Starter in pages/index.js we can see this particularly happening in this bit of code:

1const [imageSrc, setImageSrc] = useState();
2
3function handleOnChange(changeEvent) {
4 const reader = new FileReader();
5
6 reader.onload = function(onLoadEvent) {
7 setImageSrc(onLoadEvent.target.result);
8 setUploadData(undefined);
9 }
10
11 reader.readAsDataURL(changeEvent.target.files[0]);
12}

Where we read the value of the file-picker and store the data.

With our Upload endpoint, we can now pass this right along using a POST request, which will tell Cloudinary we want to upload that image, and get a URL and it's details in response.

To do this, we have a Submit button hooked up to the handleOnSubmit function where inside, we're going to perform our upload.

Update the handleOnSubmit function to:

1async function handleOnSubmit(event) {
2 event.preventDefault();
3
4 const results = await fetch('/api/upload', {
5 method: 'POST',
6 body: JSON.stringify({
7 image: imageSrc
8 })
9 }).then(r => r.json());
10
11 setUploadData(results);
12}

Here we are:

  • Setting up an async function
  • Prevent default to avoid the default browser actions for submitting a form
  • Pass along our image source to our API endpoint
  • Take the results and store it in local state

Now inside of our app, let's replace the locally selected image with our newly uploaded image.

Inside of our form we'll see where we're adding the image using imageSrc. Let's replace that with the following:

1{ imageSrc && !uploadData && (
2 <img src={imageSrc} />
3)}
4
5{ uploadData && (
6 <img src={uploadData.secure_url} />
7)}

We're saying if we have an image source but no upload data, show the locally stored image, otherwise, show the image we uploaded.

At this point, you shouldn't even notice a difference, as the uploaded image should the same as the local image.

But now we can kick off a separate request to remove our background.

Follow along with the commit!

Step 4: Removing the background of an image on upload

In our last step, we uploaded our local image to Cloudinary, but now, we want to do the same thing only this time, remove the background image.

The tricky thing is this is an asynchronous process, where uploading the image kicks off the request, but then we need to keep checking until it's done in order to use it.

So to start, we're going to utilize our existing upload, passing that URL to Cloudinary saying we wnat to upload the same image, but remove the background.

In order for this to work, you need to have the Cloudinary AI Background Removal add-on installed on your Cloudinary account.

Note: high usage costs money, but the free tier can let you play around with it!

But once we're ready to go, we're going to use React's useEffect hook and say that whenever we have uploaded data available, we're going to kick off a second request to upload.

This will involve a few steps.

First, let's import useEffect from React:

1import { useEffect, useState } from 'react';

Then we want to store this transparent image upload separately, so let's set up a new instance of state:

1const [transparentData, setTransparentData] = useState();

Inside of our page, we want to show our upload data and uploaded image, so under the uploadData add:

1{ uploadData && !transparentData && (
2 <code><pre>Loading...</pre></code>
3)}
4
5{transparentData && (
6 <code><pre>{JSON.stringify(transparentData, null, 2)}</pre></code>
7)}

We're also adding a check if our upload data is available but not the transparent data so that we have some kind of basic loading indicator.

And finally we want to perform the upload itself, so add the following under our transparentData state instance:

1useEffect(() => {
2 if ( !uploadData ) return;
3 (async function run() {
4 const results = await fetch('/api/upload', {
5 method: 'POST',
6 body: JSON.stringify({
7 image: uploadData.secure_url,
8 options: {
9 background_removal: 'cloudinary_ai'
10 }
11 })
12 }).then(r => r.json());
13
14 const transparentResult = await checkStatus();
15
16 setTransparentData(transparentResult);
17
18 async function checkStatus() {
19 const resource = await fetch(`/api/resource/?publicId=${results.public_id}`).then(r => r.json());
20 if (resource.info.background_removal.cloudinary_ai.status === 'pending') {
21 await new Promise((resolve) => setTimeout(resolve, 100));
22 return await checkStatus();
23 }
24 return resource;
25 }
26 })();
27},[uploadData, setTransparentData]);

This is a big one, so let's break it down:

  • We're using useEffect and saying that any time uploadData or our setTransparentData function changes, we want to fire it
  • Inside, we're first making sure we have uploaded data before trying to use it, otherwise returning
  • Then we're wrapping everything in a self-invoking async function to allow us to use async/await syntax (personal preference)
  • In our async function, we're firing our upload using the URL from our uploaded data
  • We're additionally passing in an option telling Cloudinary we want to use the cloudinary_ai background removal tool
  • Once that's completed, we have our base image uploaded, but our background isn't removed, so we need to keep checking until it's complete, so...
  • We create a checkStatus function that recursively keeps checking the resource status using our Resource endpoint
  • Once that endpoint returns the response that the removal was complete, it returns the resource back
  • At which point we store that transparent data locally

So after all of that, we can now try to upload an image again, and if we're patient waiting a few seconds after our first successful upload, we should see that below our uploaded data, we should see our transparent data!

We can even show that image instead of our original by replacing the image at the top:

1{ uploadData && (
2 <img src={transparentData?.secure_url || uploadData.secure_url} />
3)}

And we should now see it on the page!

Follow along with the commit!

Step 5: Installing and configuring the Cloudinary URL Gen SDK

At this point, we have our images uploaded to Cloudinary, however, we're currently just using the URLs "as is".

We want to be able to transform our image so that we can add some fun backgrounds.

To do this, we're going to use the Cloudinary URL Gen SDK, which will allow us to create our transformations in our React app, where the Node SDK is only available in Node (like our serverless function).

First let's install the URL Gen SDK with:

1yarn add @cloudinary/url-gen
2# or
3npm install @cloudinary/url-gen

Then we can configure our SDK similar to how we configured our Node SDK, however this time, we just need to use the Cloud Name.

Note: this is why we prefixed our Cloud Name with NEXT_PUBLIC earlier!

At the top of pages/index.js import the SDK:

1import { Cloudinary } from '@cloudinary/url-gen';

Then we can configure it with:

1const cloudinary = new Cloudinary({
2 cloud: {
3 cloudName: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
4 },
5 url: {
6 secure: true,
7 },
8});

We can even set up our existing images to use the SDK to prepare ourselves to change the background.

First let's create our images:

1const mainImage = uploadData && cloudinary.image(uploadData.public_id).toURL();
2const transparentImage = transparentData && cloudinary.image(transparentData.public_id).toURL()

Then we can use them in the code:

1{ mainImage && (
2 <img src={transparentImage || mainImage} />
3)}

But now at this point, our pages and images should look exactly the same when loading and uploading, except the URLs are now generated using the Cloudinary URL Gen SDK!

Follow along with the commit!

Step 6: Changing the background of transparent images using underlays

For changing our background, we're going to use a feature called underlays, meaning, we're going to create a new layer underneath our base layer.

This is opposite of overlays, where we would place it on top, but this gives us an easy way to set up an image behind our base image.

To do this, we're going to use images uploaded to our Cloudinary account, and specifically, we're going to reference these images by Public ID.

So before we dive in, find some images that you think will be fun backgrounds, and upload them to your Cloudinary account.

Note: if you want to follow along, you can download the images I'm using from GitHub: https://github.com/colbyfayock/my-image-background/tree/main/public/images

We'll want to collect all of the Public IDs of those images.

Where then we'll create an array with them to use in our app.

For instance, mine using the IDs in the screenshot above would look like:

1const BACKGROUNDS = [
2 'the-office_xvxmat',
3 'moon-earth_rvkn3k',
4 'this-is-fine_zfmbra',
5 'mario_bmvvqb'
6];

Then, we can use those backgrounds to create a thumbnail selection UI that will allow someone to select the background they want.

Above our image, let's add some controls:

1{transparentImage && (
2 <>
3 <h3>Backgrounds</h3>
4 <ul style={{
5 display: 'flex',
6 justifyContent: 'center',
7 listStyle: 'none',
8 margin: 0,
9 padding: 0,
10 }}>
11 {BACKGROUNDS.map(backgroundId => {
12 return (
13 <li key={backgroundId} style={{ margin: '0 .5em' }}>
14 <button
15 style={{
16 padding: 0,
17 cursor: 'pointer',
18 border: background === backgroundId ? 'solid 3px blueviolet' : 0
19 }}
20 onClick={() => setBackground(backgroundId)}
21 >
22 <img
23 style={{ display: 'block' }}
24 width={100}
25 src={cloudinary.image(backgroundId).resize('w_200').toURL()}
26 alt="backgroundId"
27 />
28 </button>
29 </li>
30 )
31 })}
32 </ul>
33 </>
34)}

Note: I'm just using some inline styles here to make it a little bit more usable.

Here we're:

  • Adding a new section if we have a transparent image
  • Using an unordered list to list out each available background image
  • Setting up the image URL with resizing using Cloudinary to deliver only the size we need for the thumbnail
  • Using an onClick handler to set the background ID whenever an image is selected

Now because we're storing the active background ID in state, we need to create that new instance of state.

At the top of the file add:

1const [background, setBackground] = useState();

But now that we have our background image, we can use it inside of our main image as a transformation.

Let's first create a new image variable:

1let transformedImage;
2
3if ( transparentData && background ) {
4 transformedImage = cloudinary.image(transparentData.public_id);
5
6 transformedImage.addTransformation(`u_${background},c_fill,w_1.0,h_1.0,fl_relative`);
7
8 transformedImage = transformedImage.toURL();
9}

Here we're saying:

  • First we create a variable using let because we only want to define this under conditions, which are complicated, so we're avoiding doing it in a single line
  • If we have transparent data and a background selected
  • Create a new image instance using our transparent image
  • Add a transformation of an underlay (u) where we use a crop of fill with a relative width and height set to 1.0 to allow us to make sure it just fills the background and doesn't resize the image
  • Finally turn it into a URL

To use this new image we can simply tack it on to our existing image:

1{ mainImage && (
2 <img src={transformedImage || transparentImage || mainImage} />
3)}

But once we upload an image and select a background, we'll see our new image!

Follow along with the commit!

Bonus

In my demo at the top, you'll notice it's slightly different, here are a few bonus additions to the project not necessarily relevant to the tutorial that will help clean things up a bit.

Colby Fayock

Sr Dev Experience Engineer @ Cloudinary

Astrocoder, Dev Experience Engineer, Space Jelly Commander—I help others get the tech out of the way to solve real problems with the tools of the web. I work with the dev community at Cloudinary and am a prolific creator of educational content around the web teaching others through learning by doing one Star Wars plush cuddle at a time.