Skip to content

Image Color Palette Generator

In this short tutorial, we’ll look at how we can extract colors from an image, generate a color palette and use it to style different elements using CSS Variables.

The final project can be viewed on Codesandbox.

The completed project is available on Codesandbox.

Ensure you have Node.js and NPM installed. Have a look at the official Node.js website to learn how you can install it. This tutorial also assumes that you have basic knowledge of Javascript, Node.js, and React/Next.js.

We’re going to be using Cloudinary to store our images. Cloudinary provides an API that allows us to store and optimize media. It’s easy to get started and you can do it for free. They also have amazing documentation that is easy to follow. Let’s get started.

Sign in to Cloudinary or create a new account. Once that’s done, make your way to the console. You will notice your API credentials in the top left corner.

Cloudinary Dashboard

Pay particular attention to your Cloud name API Key and API Secret. You can take note of these since we’ll be using them later.

The first thing we need to do is create a new Next js project. Open your terminal/command line in your desired folder and run the following command.


npx create-next-app

You will be prompted to give your application a name. Just give it any appropriate name. If you’re following along, I named mine `nextjs-color-palette-generator. This will create a basic Next.js app. If you’d like to use features such as Typescript, have a look at the official docs. Switch into the newly created project.


cd nextjs-color-palette-generator

Finally, open your project in your favorite code editor.

Before we proceed any further, let’s install the Cloudinary NPM package. We’ll use this to communicate with their API


npm install --save cloudinary

Create a new folder called lib at the root of your new project. Inside the lib folder, create a file called cloudinary.js and paste the following code inside.


// Import the v2 api and rename it to cloudinary

import  {  v2  as  cloudinary  }  from  "cloudinary";

  

// Initialize the SDK with cloud_name, api_key, and api_secret

cloudinary.config({

cloud_name:  process.env.CLOUD_NAME,

api_key:  process.env.API_KEY,

api_secret:  process.env.API_SECRET,

});

  

export  const  handleCloudinaryUpload  =  (path)  =>  {

// Create and return a new Promise

return  new  Promise((resolve,  reject)  =>  {

// Use the SDK to upload media

cloudinary.uploader.upload(

path,

{

// Folder to store video in

folder:  "images/",

// Type of resource

resource_type:  "image",

},

(error,  result)  =>  {

if  (error)  {

// Reject the promise with an error if any

return  reject(error);

}

  

// Resolve the promise with a successful result

return  resolve(result);

}

);

});

};

Code language: JavaScript (javascript)

We first import the v2 API and rename it to cloudinary for better readability. We then initialize the SDK by calling the config method with the cloud_name, api_key, and api_secret. We’ve used environment variables that we have not defined yet. Let’s do that. Create a file called .env.local at the root of your project and paste the following inside


CLOUD_NAME=YOUR_CLOUD_NAME

API_KEY=YOUR_API_KEY

API_SECRET=YOUR_API_SECRET

Don’t forget to replace YOUR_CLOUD_NAME YOUR_API_KEY and YOUR_API_SECRET with the appropriate values that we got from the Obtaining Cloudinary Credentials section above. You can learn more about support for environment variables in Next.js from the official docs

In our lib/cloudinary.js file we also have a function called handleCloudinaryUpload. This function takes in a path to the file we want to upload. We then call the uploader.upload method on the SDK. Read more about the upload options from the official documentation. That’s it for that file. Let’s move on to the next step.

API routes are a core concept of Next.js. I highly recommend you have some knowledge of how they work. The official docs is a great place to get started.

Create a new file called images.js under the pages/api folder. Paste the following code inside


// pages/api/images.js

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

import  {  handleCloudinaryUpload  }  from  "../../lib/cloudinary";

  

export  const  config  =  {

api:  {

bodyParser:  false,

},

};

  

export  default  async  function  handler(req,  res)  {

switch  (req.method)  {

case  "POST":  {

try  {

const  result  =  await  handlePostRequest(req);

  

return  res.status(200).json({ message:  "Success",  result  });

}  catch  (error)  {

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

default:  {

return  res.status(405).json({ message:  "Method not allowed"  });

}

}

}

  

const  handlePostRequest  =  async  (req)  =>  {

const  data  =  await  parseForm(req);

  

const  uploadResult  =  await  handleCloudinaryUpload(data?.files?.file.path);

  

return  {  uploadResult  };

};

Code language: JavaScript (javascript)

We’re importing the handleCloudinaryUpload function we created earlier. We’re going to be using a custom parser to get the uploaded file so we’re using a custom config for our route’s API middleware. Our API route handle switches the HTTP request method to handle only the POST request and returning a failure response for all other HTTP methods. In our handlePostRequest we parse the incoming form to get the uploaded file then upload that file to cloudinary and return the upload result. You’ll quickly notice that we haven’t defined parseForm yet. Now is a good time to do that.

We will use a package called Formidable to parse the form. Run the following in your terminal, inside your project folder’s root to install.


npm install --save formidable

Add the following import at the top of pages/api/images.js


// pages/api/images.js

import  {  IncomingForm,  Fields,  Files  }  from  "formidable";

Code language: JavaScript (javascript)

and add the following function in the same file :


// pages/api/images.js

  

/**

*

* @param  {*}  req

* @returns  {Promise<{ fields:Fields; files:Files; }>}

*/

const  parseForm  =  (req)  =>  {

return  new  Promise((resolve,  reject)  =>  {

const  form  =  new  IncomingForm({ keepExtensions:  true, multiples:  true  });

  

form.parse(req,  (error,  fields,  files)  =>  {

if  (error)  {

return  reject(error);

}

  

return  resolve({  fields,  files  });

});

});

};

Code language: JavaScript (javascript)

Read about the Formidable API to better understand what’s happening here. We’re creating a new incoming form and then using that to parse the incoming request that includes the image being uploaded.

There’s one last piece to the puzzle. We need to generate a color palette from the image. We can do this either on the frontend after we’ve uploaded our image to cloudinary or do it on the backend before we upload the image to cloudinary. We’ll go with the latter. Let me explain the decision. With cloudinary, you can apply transformations to your image before uploading. Have a look at the Transformation URL docs and the Upload docs. If we have a color palette ready before we upload the image we can use some of the colors and apply them to our transformations. Enough talk, let’s implement it.

Install the node-vibrant package


npm install --save node-vibrant

Add the following import to the top of pages/api/images.js


// pages/api/images.js

  

import  *  as  Vibrant  from  "node-vibrant";

Code language: JavaScript (javascript)

Modify handlePostRequest to read like so :


const  handlePostRequest  =  async  (req)  =>  {

const  data  =  await  parseForm(req);

  

const  palette  =  await  Vibrant.from(data?.files?.file.path).getPalette();

  

const  uploadResult  =  await  handleCloudinaryUpload(data?.files?.file.path);

  

return  {  palette,  uploadResult  };

};

Code language: JavaScript (javascript)

Now we’re using the node-vibrant package to generate a color palette from the image then proceeding to upload the image to cloudinary and returning both the palette and the upload result. With this, if you wish to apply transformations to your images using the colors you can do so as you upload. We won’t be doing that in this tutorial though.

Here’s the complete pages/api/images.js


// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

import  {  IncomingForm,  Fields,  Files  }  from  "formidable";

import  *  as  Vibrant  from  "node-vibrant";

import  {  handleCloudinaryUpload  }  from  "../../lib/cloudinary";

  

export  const  config  =  {

api:  {

bodyParser:  false,

},

};

  

export  default  async  function  handler(req,  res)  {

switch  (req.method)  {

case  "POST":  {

try  {

const  result  =  await  handlePostRequest(req);

  

return  res.status(200).json({ message:  "Success",  result  });

}  catch  (error)  {

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

default:  {

return  res.status(405).json({ message:  "Method not allowed"  });

}

}

}

  

const  handlePostRequest  =  async  (req)  =>  {

const  data  =  await  parseForm(req);

  

const  palette  =  await  Vibrant.from(data?.files?.file.path).getPalette();

  

const  uploadResult  =  await  handleCloudinaryUpload(data?.files?.file.path);

  

return  {  palette,  uploadResult  };

};

  

/**

*

* @param  {*}  req

* @returns  {Promise<{ fields:Fields; files:Files; }>}

*/

const  parseForm  =  (req)  =>  {

return  new  Promise((resolve,  reject)  =>  {

const  form  =  new  IncomingForm({ keepExtensions:  true, multiples:  true  });

  

form.parse(req,  (error,  fields,  files)  =>  {

if  (error)  {

return  reject(error);

}

  

return  resolve({  fields,  files  });

});

});

};

Code language: JavaScript (javascript)

With that, we’re now ready to move on to the front end.

CSS variables are a powerful tool in web frontend development. And today we’re going to be leveraging their power. MDN Web Docs define them as entities defined by CSS authors that contain specific values to be reused throughout a document. Read more about them from MDN Web Docs

Open styles/globals.css and paste the following code inside


:root  {

--primary-color:  #001aff;

--secondary-color:  #ffd000;

--background-color:  #ae00ff;

}

Code language: CSS (css)

What we’ve done here is define three CSS variables namely primary-color, secondary-color, and background-color. These can be named whatever you want and can be any valid CSS value. For our use case, we're using them to define three colors. The variables are defined under the :root` selector which selects the root node, usually the html element.

Open pages/index.js and paste the following code inside


import  Head  from  "next/head";

import  Image  from  "next/image";

import  {  useState  }  from  "react";

import  {  Palette  }  from  "@vibrant/color";

  

export  default  function  Home()  {

/**

* Holds the selected image file

* @type  {[File,Function]}

*/

const  [file,  setFile]  =  useState(null);

  

/**

* Holds the uploading/loading state

* @type  {[boolean,Function]}

*/

const  [loading,  setLoading]  =  useState(false);

  

/**

* Holds the result of the upload. This contains the cloudinary upload result and the color palette

* @type  {[{palette:Palette,uploadResult:UploadApiResponse},Function]}

*/

const  [result,  setResult]  =  useState();

  

const  handleFormSubmit  =  async  (e)  =>  {

e.preventDefault();

  

setLoading(true);

try  {

const  formData  =  new  FormData(e.target);

  

const  response  =  await  fetch("/api/images",  {

method:  "POST",

body:  formData,

});

  

const  data  =  await  response.json();

  

if  (response.ok)  {

setResult(data.result);

  

// Get the root document

const  htmlDoc  =  document.querySelector("html");

  

// Set the primary color CSS variable to the palette's DarkVibrant color

htmlDoc.style.setProperty(

"--primary-color",

`rgb(${data.result.palette.DarkVibrant.rgb.join("  ")})`

);

  

// Set the secondary color CSS variable to the palette's Muted color

htmlDoc.style.setProperty(

"--secondary-color",

`rgb(${data.result.palette.Muted.rgb.join("  ")})`

);

  

// Set the background color CSS variable to the palette's Vibrant color

htmlDoc.style.setProperty(

"--background-color",

`rgb(${data.result.palette.Vibrant.rgb.join("  ")})`

);

  

return;

}

  

throw  data;

}  catch  (error)  {

// TODO: Show error message to the user

console.error(error);

}  finally  {

setLoading(false);

}

};

  

return  (

<div className="">

<Head>

<title>Generate Color Palette with Next.js</title>

<meta

name="description"

content="Generate Color Palette with Next.js"

/>

<link rel="icon"  href="/favicon.ico"  />

</Head>

  

<main className="container">

<div className="header">

<h1>Generate Color Palette with Next.js</h1>

</div>

{!result  &&  (

<form className="upload"  onSubmit={handleFormSubmit}>

{file  &&  <p>{file.name} selected</p>}

<label htmlFor="file">

<p>

<b>Tap To Select Image</b>

</p>

</label>

<br />

<input

type="file"

name="file"

id="file"

accept=".jpg,.png"

multiple={false}

required

disabled={loading}

onChange={(e)  =>  {

const  file  =  e.target.files[0];

  

setFile(file);

}}

/>

<button type="submit"  disabled={loading  ||  !file}>

Upload Image

</button>

</form>

)}

{loading  &&  (

<div className="loading">

<hr />

<p>Please wait as the image uploads</p>

<hr />

</div>

)}

{result  &&  (

<div className="image-container">

<div className="image-wrapper">

<Image

className="image"

src={result.uploadResult.secure_url}

alt={result.uploadResult.secure_url}

layout="fill"

></Image>

<div className="palette">

{Object.entries(result.palette).map(([key,  value],  index)  =>  (

<div

key={index}

className="color"

style={{

backgroundColor:  `rgb(${value.rgb.join("  ")})`,

}}

>

<b>{key}</b>

</div>

))}

</div>

</div>

</div>

)}

</main>

</div>

);

}

Code language: JavaScript (javascript)

A basic react component. We have a few useState hooks to store the selected image file state, loading state, and the result from the call to /api/images endpoint. We also have a function that will handle the form submission. The function posts the form data to the /api/images endpoint that we created earlier. It then updates the resulting state with the result. Remember that the result contains the generated palette and the cloudinary upload result. The function then updates the CSS variables that we just defined to a few colors from the palette. The palette contains 6 color swatches: Vibrant,DarkVibrant,LightVibrant,Muted,DarkMuted,LightMuted. Here we’re only using the DarkVibrant,Muted, and Vibrant swatches to set the --primary-color, –secondary-color, and –background-color variables respectively. Here’s how you can get the actual color from a Swatch.

Moving on to the HTML, we have a form and input for image selection. Below that we have a container that will show the uploaded image and also a container that shows the colors in the palette. The colors on the page have been set to the CSS variables we defined. Once the image has been uploaded, the variables are set to some colors from the generated palette, consequently, the colors on the page will change to match those in the image.

Here’s the full code for pages/index.js, including the CSS


import  Head  from  "next/head";

import  Image  from  "next/image";

import  {  useState  }  from  "react";

import  {  Palette  }  from  "@vibrant/color";

  

export  default  function  Home()  {

/**

* Holds the selected image file

* @type  {[File,Function]}

*/

const  [file,  setFile]  =  useState(null);

  

/**

* Holds the uploading/loading state

* @type  {[boolean,Function]}

*/

const  [loading,  setLoading]  =  useState(false);

  

/**

* Holds the result of the upload. This contains the cloudinary upload result and the color palette

* @type  {[{palette:Palette,uploadResult:UploadApiResponse},Function]}

*/

const  [result,  setResult]  =  useState();

  

const  handleFormSubmit  =  async  (e)  =>  {

e.preventDefault();

  

setLoading(true);

try  {

const  formData  =  new  FormData(e.target);

  

const  response  =  await  fetch("/api/images",  {

method:  "POST",

body:  formData,

});

  

const  data  =  await  response.json();

  

if  (response.ok)  {

setResult(data.result);

  

// Get the root document

const  htmlDoc  =  document.querySelector("html");

  

// Set the primary color CSS variable to the palette's DarkVibrant color

htmlDoc.style.setProperty(

"--primary-color",

`rgb(${data.result.palette.DarkVibrant.rgb.join("  ")})`

);

  

// Set the secondary color CSS variable to the palette's Muted color

htmlDoc.style.setProperty(

"--secondary-color",

`rgb(${data.result.palette.Muted.rgb.join("  ")})`

);

  

// Set the background color CSS variable to the palette's Vibrant color

htmlDoc.style.setProperty(

"--background-color",

`rgb(${data.result.palette.Vibrant.rgb.join("  ")})`

);

  

return;

}

  

throw  data;

}  catch  (error)  {

// TODO: Show error message to the user

console.error(error);

}  finally  {

setLoading(false);

}

};

  

return  (

<div className="">

<Head>

<title>Generate Color Palette with Next.js</title>

<meta

name="description"

content="Generate Color Palette with Next.js"

/>

<link rel="icon"  href="/favicon.ico"  />

</Head>

  

<main className="container">

<div className="header">

<h1>Generate Color Palette with Next.js</h1>

</div>

{!result  &&  (

<form className="upload"  onSubmit={handleFormSubmit}>

{file  &&  <p>{file.name} selected</p>}

<label htmlFor="file">

<p>

<b>Tap To Select Image</b>

</p>

</label>

<br />

<input

type="file"

name="file"

id="file"

accept=".jpg,.png"

multiple={false}

required

disabled={loading}

onChange={(e)  =>  {

const  file  =  e.target.files[0];

  

setFile(file);

}}

/>

<button type="submit"  disabled={loading  ||  !file}>

Upload Image

</button>

</form>

)}

{loading  &&  (

<div className="loading">

<hr />

<p>Please wait as the image uploads</p>

<hr />

</div>

)}

{result  &&  (

<div className="image-container">

<div className="image-wrapper">

<Image

className="image"

src={result.uploadResult.secure_url}

alt={result.uploadResult.secure_url}

layout="fill"

></Image>

<div className="palette">

{Object.entries(result.palette).map(([key,  value],  index)  =>  (

<div

key={index}

className="color"

style={{

backgroundColor:  `rgb(${value.rgb.join("  ")})`,

}}

>

<b>{key}</b>

</div>

))}

</div>

</div>

</div>

)}

</main>

<style jsx>{`

main {

width: 100%;

height: 100vh;

background-color: var(--background-color);

display: flex;

flex-flow: column;

justify-content: flex-start;

align-items: center;

}

  

main .header {

width: 100%;

display: flex;

justify-content: center;

align-items: center;

background-color: var(--secondary-color);

padding: 0 40px;

color: white;

}

  

main .header h1 {

-webkit-text-stroke: 1px #000000;

}

  

main .loading {

color: white;

}

  

main form {

width: 50%;

padding: 20px;

display: flex;

flex-flow: column;

justify-content: center;

align-items: center;

border-radius: 5px;

margin: 20px auto;

background-color: #ffffff;

}

  

main form label {

height: 100%;

width: 100%;

display: flex;

justify-content: center;

align-items: center;

cursor: pointer;

background-color: #777777;

color: #ffffff;

border-radius: 5px;

}

  

main form label:hover:not([disabled]) {

background-color: var(--primary-color);

}

  

main form input {

opacity: 0;

width: 0.1px;

height: 0.1px;

}

  

main form button {

padding: 15px 30px;

border: none;

background-color: #e0e0e0;

border-radius: 5px;

color: #000000;

font-weight: bold;

font-size: 18px;

}

  

main form button:hover:not([disabled]) {

background-color: var(--primary-color);

color: #ffffff;

}

  

main div.image-container {

position: relative;

width: 100%;

flex: 1 0;

}

  

main div.image-container .image-wrapper {

position: relative;

margin: auto;

width: 80%;

height: 100%;

}

  

main div.image-container div.image-wrapper .image-wrapper .image {

object-fit: cover;

}

  

main div.image-container .image-wrapper .palette {

width: 100%;

height: 150px;

position: absolute;

bottom: 0;

left: 0;

background-color: rgba(255, 255, 255, 50%);

display: flex;

flex-flow: row nowrap;

justify-content: flex-start;

}

  

main div.image-container .image-wrapper .palette .color {

flex: 1;

margin: 5px;

}

  

main div.image-container .image-wrapper .palette .color b {

background-color: #ffffff;

padding: 0 5px;

}

`}</style>

</div>

);

}

Code language: JavaScript (javascript)

There’s one final thing we need to do. We’re using the Image component from Next.js which optimizes images. Read about it here. When we use this component to load and display external images, we need to add the respective domains to a whitelist. This is better explained here. For our use case, we need to add the Cloudinary domain.

Open next.config.js and modify the code to include images config in the module exports


// next.config.js

  

module.exports  =  {

// ... other settings

images:  {

domains:  ["res.cloudinary.com"],

},

};

  

Code language: JavaScript (javascript)

Our App is ready. You can preview it by running


npm run dev

Now, go ahead and select an image and upload it. Once the upload is complete you’ll notice that the color scheme of the page changes because the CSS variables we defined in styles/globals.css are changed dynamically and set to some colors from the generated palette.

You can find the full code on my Github. https://github.com/musebe/Color-Pallete-Generator.git

Back to top

Featured Post