Skip to content

RESOURCES / BLOG

How to Create an Image Text Overlay Generator With Cloudinary and React

Want to ship a quick banner maker without wrangling a custom canvas stack? In this post, you’ll build a React-based image text overlay generator powered by Cloudinary. Users will upload an image straight to Cloudinary, then layer editable text, adjust its reposition, resize, tweak styling, and see instant previews driven by Cloudinary’s on-the-fly transformations. We’ll wire up the upload flow, model the overlay with React state, and generate URLs that encode text, size, and coordinates so the final asset is production-ready.

Man sitting on a roof with the text JUST DO IT behind him

Retrieve your Cloudinary cloud name, API key, and secret to make sure your MCP server configurations are connected to your Cloudinary application.

To begin, log in to your Cloudinary account or create a free account. If prompted with the question, “What’s your main interest?”, select Coding with APIs and SDKs or Skip.

Cloudinary login page with "What's your main interest" and two options: Coding with APIs and SDKs and Interactive Digital Asset Management

In your account, select Settings > Product Environments. Here, you’ll see the cloud that you’ve created. Let’s click the three-dot menu to edit your Cloudinary cloud name.

Product Environments. Active is toggled on.

Edit the product environment and name your cloud, then click Save Changes.

It’s essential to maintain the same cloud name across different tools for consistency.

Product Environment. Display name reads DevX Demo.

In this tutorial, I’m using Vite to build my React application. I recommend that you do the same. Follow the instructions on Vite’s official website to create a React application.

In your App.tsx file, replace the existing code with the following code. The main purpose of this file is to pass the state to the different components of the app.

import React, { useState } from 'react';

import LeftSideBar from './LeftSideBar';

import Playground from './Playground';

export interface Settings {

  text: string;

  fontFamily: string;

  color: string;

  fontSize: number;

  rotation?: number;

  x: number;

  y: number;

  image: string | null;

}

const App: React.FC = () => {

  const [settings, setSettings] = useState<Settings>({

    text: 'Cloudinary',

    fontFamily: 'Arial',

    color: '#000000',

    fontSize: 30,

    x: 0,

    y: 0,

    image: null,

  });

  const [image, setImage] = useState('')

  const updateSettings = (updated: Partial<Settings>) => {

    setSettings((prev) => ({

      ...prev,

      ...updated,

    }));

  };

  return (

    <div className="flex">

      <LeftSideBar settings={settings} updateSettings={updateSettings} setImage={setImage} />

      <Playground settings={settings} image={image}/>

    </div>

  );

};

export default App;Code language: JavaScript (javascript)

Now, you’ll create a LeftSideBar component. This component will be in charge of handling the logic for adjusting the settings of the text, including the color, size, position, and font family.

import React, { ChangeEvent } from 'react';

import { Settings } from './App';

import CloudinaryUploadWidget from './CloudinaryUploadWidget';

interface LeftSideBarProps {

  settings: Settings;

  updateSettings: (updated: Partial<Settings>) => void;

  setImage: React.Dispatch<React.SetStateAction<string>>;

}

const LeftSideBar: React.FC<LeftSideBarProps> = ({ settings, updateSettings, setImage }) => {

  // Handlers for the various inputs

  const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => {

    updateSettings({ text: e.target.value });

  };

  const handleFontChange = (e: ChangeEvent<HTMLSelectElement>) => {

    updateSettings({ fontFamily: e.target.value });

  };

  const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => {

    updateSettings({ color: e.target.value });

  };

  const handleFontSizeChange = (e: ChangeEvent<HTMLInputElement>) => {

    updateSettings({ fontSize: parseInt(e.target.value, 10) });

  };

  const handleXChange = (e: ChangeEvent<HTMLInputElement>) => {

    updateSettings({ x: parseInt(e.target.value, 10) });

  };

  const handleYChange = (e: ChangeEvent<HTMLInputElement>) => {

    updateSettings({ y: parseInt(e.target.value, 10) });

  };

  return (

    <div className="h-screen w-72 p-5 border-r border-gray-300">

      <h2 className="text-xl font-bold mb-4">Background Text</h2>

      <div className="mb-3">

        <label className="block text-sm font-medium">Text:</label>

        <input

          type="text"

          className="w-full border border-gray-300 rounded px-2 py-1"

          value={settings.text}

          onChange={handleTextChange}

        />

      </div>

      <div className="mb-3">

        <label className="block text-sm font-medium">Font Family:</label>

        <select

          className="w-full border border-gray-300 rounded px-2 py-1"

          value={settings.fontFamily}

          onChange={handleFontChange}

        >

          <option value="Arial">Arial</option>

          <option value="Times New Roman">Times New Roman</option>

          <option value="Courier New">Courier New</option>

          <option value="Georgia">Georgia</option>

          <option value="Verdana">Verdana</option>

        </select>

      </div>

      <div className="mb-3">

        <label className="block text-sm font-medium">Color:</label>

        <input

          type="color"

          className="w-full h-10"

          value={settings.color}

          onChange={handleColorChange}

        />

      </div>

      <div className="mb-3">

        <label className="block text-sm font-medium">Font Size (px):</label>

        <input

          type="number"

          className="w-full border border-gray-300 rounded px-2 py-1"

          value={settings.fontSize}

          onChange={handleFontSizeChange}

        />

      </div>

      <div className="mb-3">

        <label className="block text-sm font-medium">X Position (px):</label>

        <input

          type="number"

          className="w-full border border-gray-300 rounded px-2 py-1"

          value={settings.x}

          onChange={handleXChange}

        />

      </div>

      <div className="mb-3">

        <label className="block text-sm font-medium">Y Position (px):</label>

        <input

          type="number"

          className="w-full border border-gray-300 rounded px-2 py-1"

          value={settings.y}

          onChange={handleYChange}

        />

      </div>

      <div className="mb-3">

        <CloudinaryUploadWidget setImage={setImage} />

      </div>

    </div>

  );

};

export default LeftSideBar;Code language: JavaScript (javascript)

Time to create the CloudinaryUploadWidget file. This file will have a component that we’re injecting into the LeftSideBar component. The purpose of this component is to add the Cloudinary Upload widget for the user to upload images to the Cloudinary cloud. Make sure to enter your cloud name.

import { useEffect, useState, useCallback } from "react";

interface Props {

    setImage: React.Dispatch<React.SetStateAction<string>>;

  }

function CloudinaryUploadWidget({setImage}: Props) {

  const [loaded, setLoaded] = useState(false);

  const uwConfig = {

    cloudName: "YOUR CLOUDE NAME",

    uploadPreset: "upload-images",

    sources: ["local"],

    multiple: false,

    tags: ["text-overlay"],

  };

  /**

   * Load Cloudinary Upload Widget Script

   */

  useEffect(() => {

    if (!loaded) {

      const uwScript = document.getElementById("uw");

      if (!uwScript) {

        const script = document.createElement("script");

        script.setAttribute("async", "");

        script.setAttribute("id", "uw");

        script.src = "https://upload-widget.cloudinary.com/global/all.js";

        script.addEventListener("load", () => setLoaded(true));

        document.body.appendChild(script);

      } else {

        setLoaded(true);

      }

    }

  }, [loaded]);

  const initializeCloudinaryWidget = async () => {

    if (loaded) {

      try {

        await window.cloudinary.openUploadWidget(uwConfig, processUploads);

      } catch (error) {

        console.log("failed", error);

      }

    }

  };

  const processUploads = useCallback((error, result) => {

    if (result?.event === "queues-end") {

      console.log('result', result.info.files[0].name);

      setImage(result.info.files[0].name as string);

    }

  }, []);

  return (

    <>

      <button

        id="upload_widget"

        className="bg-blue-600 text-white hover:bg-blue-200 hover:text-black cursor-pointer h-[40px] rounded-lg w-full flex justify-center items-center "

        onClick={initializeCloudinaryWidget}

      >

        Upload Image

      </button>

    </>

  );

}

export default CloudinaryUploadWidget;Code language: JavaScript (javascript)

Finally, we’ll create the Playground component. This component will put all the pieces together using Cloudinary transformations. The way it works is, we have two copies of the same image and we stack them one on top of the other. The one on top will be converted into a PNG, and we’ll remove the background. Now, between these two images, we’ll add the text so the image has a nice 2D effect. Make sure to change the cloud name when initializing Cloudinary.

import React, { useRef, useMemo } from "react";

import { Settings } from "./App";

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

import { AdvancedImage } from "@cloudinary/react";

import { backgroundRemoval } from "@cloudinary/url-gen/actions/effect";

import { source } from "@cloudinary/url-gen/actions/overlay";

import { text as cloudinaryText } from "@cloudinary/url-gen/qualifiers/source";

import { TextStyle } from "@cloudinary/url-gen/qualifiers/textStyle";

import { Position } from "@cloudinary/url-gen/qualifiers";

import { compass } from "@cloudinary/url-gen/qualifiers/gravity";

import { fetch } from "@cloudinary/url-gen/qualifiers/source";

interface PlaygroundProps {

  settings: Settings;

  image: string;

}

const Playground: React.FC<PlaygroundProps> = ({ settings, image }) => {

  const containerRef = useRef<HTMLDivElement>(null);

  // 1) Only initialize Cloudinary once

  const cld = useMemo(

    () =>

      new Cloudinary({

        cloud: { cloudName: "text-overlay" },

      }),

    []

  );

  // 2) Create the “top” image pipeline (background removal) separately

  const topImage = useMemo(

    () =>

      cld

        .image(image)

        .effect(backgroundRemoval().fineEdges())

        .format("png"),

    [cld, image]

  );

  // 3) Create a fresh “background + text overlay” pipeline on every relevant change

  const backgroundWithText = useMemo(() => {

    const img = cld.image(image);

    // Apply overlay only if there’s text

    if (settings.text) {

      img.overlay(

        source(

          cloudinaryText(

            settings.text,

            new TextStyle(settings.fontFamily, settings.fontSize)

          ).textColor(settings.color)

        ).position(

          new Position()

            .gravity(compass("north_west"))

            .offsetX(Math.round(settings.x))

            .offsetY(Math.round(settings.y))

        )

      ).overlay(source(fetch(

        topImage.toURL()

      )));

    }

    return img;

  }, [

    cld,

    image,

    settings.text,

    settings.fontFamily,

    settings.fontSize,

    settings.color,

    settings.x,

    settings.y,

    topImage

  ]);

  return (

    <div className="flex-1 p-5 flex justify-center items-center flex-col gap-4">

      {image ? (

        <div ref={containerRef} className="relative inline-block overflow-hidden">

          <AdvancedImage

            cldImg={backgroundWithText}

            alt="Background with Text"

            className="block max-w-full h-auto"

          />

        </div>

      ) : (

        <p className="text-gray-500">Please upload an image.</p>

      )}

      { image && <a href={backgroundWithText.toURL()} target="_blank" className="bg-blue-600 text-white hover:bg-blue-200 hover:text-black cursor-pointer h-[40px] rounded-lg w-[200px] flex justify-center items-center">Download Image</a>}

    </div>

  );

};

export default Playground;Code language: JavaScript (javascript)

The first thing we have to do is to run npm install in the root of your project to install the frontend and backend dependencies.

Open your other terminal, run npm run dev, and navigate to http://localhost:3000/.

You now have a working React image text overlay tool powered by Cloudinary, where users can upload, edit, and preview production-ready assets in real time. With the heavy lifting handled by Cloudinary, you can focus on extending features and building creative experiences faster.

To stay updated with the latest product features, follow Cloudinary on X and explore other sample apps.

Building an app with Cloudinary and need inspiration? Visit the App Gallery.

Resources:

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free