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.

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.

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.

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.

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:
- GitHub Repo: Cloudinary-Text-Overlay-Generator