ScreenshotNeo

BlogHow-to

How to Change Text Color in Dynamically Generated Images

Set text color at render time with Cloudinary or Bannerbear, handle per-record colors, validate input, and avoid common dynamic-image failures.

By the ScreenshotNeo team29 September 20268 min read

How to Change Text Color in Dynamically Generated Images

Direct answer: pass the color as a style value when the image is rendered. Keep the text as editable text in a Cloudinary text layer or a Bannerbear template modification; do not rasterize a fixed-color bitmap and try to recolor it afterward. Cloudinary is a good fit when you want transformation URLs or SDK calls. Bannerbear is a good fit when designers maintain reusable templates and your application sends per-request modifications.

A runtime color can come from a database record, a user preference, a brand configuration, or an automated rule. Normalize it before sending it to the image service, validate contrast against the background, and cache the final result using all visual inputs as part of the cache key.

1. Choose the right rendering model

Requirement Cloudinary Bannerbear
Transformation URL or SDK control Strong fit Less URL-centric
Reusable designer-managed template Possible with layers Strong fit
Color supplied per request font_color, overlay color, or variables modifications[].color and background
RGBA and opacity Supports RGB/RGBA hex values in overlays Use the color fields supported by the template/API
Typography and placement Font, size, gravity, offsets, and layer transformations Defined by template layers plus modifications

Cloudinary’s text upload API exposes POST /image/text and a font_color option. Its overlay syntax uses a text layer (l_text) and a color qualifier (co). Cloudinary documents named colors, three- or six-digit RGB hex, and four- or eight-digit RGBA hex values; if color is omitted, the default is black. See the Upload API reference and text overlay reference.

2. Cloudinary: generate text with a color at upload time

Use the text upload endpoint when the generated image is primarily a text asset. The request carries the string, font settings, and color together, so every record can render a different style without creating a new source bitmap.

Pass the color with the text at render time so one template can produce many variants.
Pass the color with the text at render time so one template can produce many variants.

cURL

curl https://api.cloudinary.com/v1_1/YOUR_CLOUD_NAME/image/text \
  -X POST \
  -d "text=Invoice paid" \
  -d "font_family=Arial" \
  -d "font_size=72" \
  -d "font_color=%23FFFFFF" \
  -d "background=%230B1220" \
  -d "api_key=YOUR_API_KEY" \
  -d "timestamp=$(date +%s)" \
  -d "signature=YOUR_SIGNATURE"

Production Cloudinary requests normally require a signature generated with your API secret. Keep that secret on your server. Encode the leading hash in a form request as %23, or let your HTTP client encode form values.

Python

import cloudinary
import cloudinary.uploader

cloudinary.config(
    cloud_name="YOUR_CLOUD_NAME",
    api_key="YOUR_API_KEY",
    api_secret="YOUR_API_SECRET",
)

result = cloudinary.uploader.upload(
    "text:Invoice paid",
    resource_type="image",
    font_family="Arial",
    font_size=72,
    font_color="#FFFFFF",
    background="#0B1220",
)
print(result["secure_url"])

Node.js

import { v2 as cloudinary } from "cloudinary";

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

const result = await cloudinary.uploader.upload("text:Invoice paid", {
  resource_type: "image",
  font_family: "Arial",
  font_size: 72,
  font_color: "#FFFFFF",
  background: "#0B1220",
});
console.log(result.secure_url);

3. Cloudinary overlays: recolor text on a base image

For a card, thumbnail, or social image, keep a base image and add a text overlay. In a transformation URL, co sets the overlay color and l_text defines the text layer. This example places bold yellow text near the bottom:

https://res.cloudinary.com/YOUR_CLOUD_NAME/image/upload/co_rgb:FFFF00,l_text:Times_90_bold:Style/fl_layer_apply,g_south,y_20/base.jpg

The same transformation can be expressed through an SDK. A color can be a name such as black, a three- or six-digit RGB value such as #09F or #0099FF, or an RGBA value when transparency is needed. Omit the color only when black is the intended default.

Per-record colors with variables

When each customer or product has a different brand color, store that color as a user-defined variable and reference it in the text-overlay style. One transformation template can then render many colors. Keep the variable constrained to a validated allowlist or a strict hex pattern before it reaches a URL.

// Pseudocode: build a transformation from validated application data
const color = record.brandColor; // validated #RRGGBB or #RRGGBBAA
const transformation = [
  { color: color, overlay: { fontFamily: "Arial", fontSize: 72, text: record.label } },
  { gravity: "south", y: 20 },
];

4. Bannerbear: modify a template at render time

Bannerbear templates expose named layers. Send a modifications array that replaces the text and sets the layer’s color or background. Bannerbear’s documentation describes color changes for primary text, secondary text, and text containers; a value such as #FFD700 is sent in the modification object. Start with the Bannerbear API documentation for your account’s template and authentication details.

Cloudinary favors transformation control; Bannerbear favors template modifications.
Cloudinary favors transformation control; Bannerbear favors template modifications.
curl -X POST "https://api.bannerbear.com/v2/images" \
  -H "Authorization: Bearer YOUR_BANNERBEAR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "YOUR_TEMPLATE_UID",
    "modifications": [
      {"name": "headline", "text": "Invoice paid", "color": "#FFFFFF"},
      {"name": "headline_container", "background": "#0B1220"}
    ]
  }'
import requests

payload = {
    "template": "YOUR_TEMPLATE_UID",
    "modifications": [
        {"name": "headline", "text": "Invoice paid", "color": "#FFFFFF"},
        {"name": "headline_container", "background": "#0B1220"},
    ],
}
r = requests.post(
    "https://api.bannerbear.com/v2/images",
    headers={"Authorization": "Bearer YOUR_BANNERBEAR_TOKEN"},
    json=payload,
    timeout=60,
)
r.raise_for_status()
print(r.json())
const response = await fetch("https://api.bannerbear.com/v2/images", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.BANNERBEAR_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template: "YOUR_TEMPLATE_UID",
    modifications: [
      { name: "headline", text: "Invoice paid", color: "#FFFFFF" },
      { name: "headline_container", background: "#0B1220" },
    ],
  }),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());

5. Validate color before rendering

  1. Accept only named tokens or a strict hexadecimal format such as ^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$.
  2. Convert shorthand values to six digits before comparing or caching them.
  3. Reject CSS functions, URLs, quotes, and control characters. They are not needed for a color parameter and can corrupt a transformation URL.
  4. Check contrast against the actual background, including gradients and photographs. A valid hex value can still produce unreadable text.
  5. Define a fallback color for missing or invalid records, and log the record identifier rather than silently producing a black label.

6. Layout and edge cases

  • Long text: set a maximum length, enable wrapping where the service supports it, or reduce font size in steps. Test the longest real value, not only a sample title.
  • Transparent colors: RGBA is useful for overlays, but alpha can reduce contrast unpredictably over images. Composite against representative backgrounds during review.
  • Unicode: select a font that contains every script you accept. Missing glyphs may appear as boxes even when the color is correct.
  • Dark mode: store separate light and dark tokens rather than applying a blind inversion. Brand colors often need a contrast-adjusted variant.
  • Cache keys: include text, normalized color, font, size, background, template version, and locale. Otherwise an old color can be served for a newly styled record.
  • URL length: large text, encoded punctuation, or many transformation components can make URLs unwieldy. Use an SDK or signed upload when the transformation becomes large.

7. Troubleshooting

Symptom Likely cause Fix
Text is black Color was omitted, malformed, or not encoded Send a supported named/RGB/RGBA value and encode # in form data.
Invalid transformation Reserved characters in text or color changed URL syntax Use SDK builders or URL-encode every dynamic value; validate the color first.
Bannerbear layer does not change Modification name does not match the template layer Copy the exact layer name/UID from the template and confirm the layer is text.
Color looks different Alpha blending, color profile, or a background image changes perception Remove alpha while debugging, compare on the final background, and keep output color settings consistent.
Glyphs are missing Selected font lacks the required characters Choose a font with the needed Unicode coverage and test all supported locales.
Old color keeps appearing CDN or application cache key excludes color Normalize the color and include it in the cache key or version the template.
Request succeeds but image is blank Text is outside the canvas or hidden behind another layer Check gravity, offsets, layer order, and minimum canvas dimensions.

8. Performance, reliability, and cost

Render only when an input changes. Cache by a deterministic key and serve the resulting asset from a CDN. For bulk jobs, queue requests and retry transient failures with exponential backoff and an idempotency key where the provider supports one. Record the provider request ID, template version, normalized color, and final asset URL so a failed render can be reproduced.

Do not put provider secrets in browser JavaScript. A small server endpoint can validate user input, sign the request, submit the render, and return a short-lived or public asset URL according to your access requirements. Estimate cost from the number of unique renders, not page views: a cache hit should not create another image request. Monitor failures separately from successful renders so a retry loop does not multiply spend.

9. Or skip the browser setup

If your next step is capturing the generated page rather than building an image-generation pipeline, ScreenshotNeo provides a single website screenshot request. It can capture PNG, JPEG, WebP, or PDF output and supports custom CSS and JavaScript, element selectors, device presets, dark mode, transparent backgrounds, waiting rules, headers, cookies, geolocation, caching, signed links, asynchronous jobs, and bulk capture.

See the ScreenshotNeo API documentation for the complete option list. A minimal request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers identify the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

10. FAQ

Can I use a CSS color name?

Cloudinary overlays accept named colors. Hex values are easier to validate and make cache keys deterministic.

Can I change color without creating a new template?

Yes. Cloudinary variables and Bannerbear modifications let the request supply a different color while reusing the same design.

Should I store colors with or without the hash?

Choose one canonical representation in your application, then add the hash only when constructing the provider parameter. This avoids duplicate cache entries.

How do I recolor text in an already rasterized image?

Re-render from the original text layer whenever possible. Pixel-level replacement is fragile around anti-aliased edges and can alter nearby colors.

Which approach is better for a design team?

Use a managed template workflow such as Bannerbear when designers frequently adjust layouts. Use Cloudinary when transformations, SDKs, and URL delivery are central to your application.