{"id":40565,"date":"2026-10-22T07:00:00","date_gmt":"2026-10-22T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=40565"},"modified":"2026-09-29T16:03:47","modified_gmt":"2026-09-29T23:03:47","slug":"auto-generate-alt-text-uploaded-image","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","title":{"rendered":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Alt text helps people understand an image when they use a screen reader or when the image can\u2019t load. The <a href=\"https:\/\/www.w3.org\/TR\/WCAG22\/#non-text-content\">Web Content Accessibility Guidelines (WCAG) 2.2<\/a> Success Criterion 1.1.1 requires meaningful non-text content to have a text alternative that serves the same purpose. This requirement makes visual information more perceivable for people who are blind, have low vision, or use other assistive technologies.<\/p>\n<p>In this tutorial, you\u2019ll build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image\u2019s HTML <code>alt<\/code> attribute. This workflow supports accessible publishing by making text alternatives part of the image lifecycle instead of a task that can be missed before publication. The <a href=\"https:\/\/cloudinary.com\/documentation\/accessible_media_images\">Cloudinary image accessibility guide<\/a> explains how text alternatives, centralized metadata, and AI captioning work together.<\/p>\n<ul>\n<li>\n<strong>Live Demo:<\/strong> <a href=\"https:\/\/react-cloudinary-alt-text.vercel.app\/\">https:\/\/react-cloudinary-alt-text.vercel.app\/<\/a>\n<\/li>\n<li>\n<strong>Source Code:<\/strong> <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\">https:\/\/github.com\/musebe\/react-cloudinary-alt-text<\/a>\n<\/li>\n<\/ul>\n<blockquote>\n<p><strong>TL;DR:<\/strong> Cloudinary AI Content Analysis generates a caption during upload. An upload preset saves that caption in Cloudinary\u2019s built-in <code>alt<\/code> contextual metadata field. A server API reads the image and metadata from Cloudinary. React then renders both through <code>AdvancedImage<\/code>.<\/p>\n<\/blockquote>\n<h2>What You\u2019ll Build<\/h2>\n<p>Altelier is a small accessibility studio for blog images. It demonstrates the complete lifecycle of accessible media:<\/p>\n<ol>\n<li>A visitor uploads an image with the Cloudinary Upload Widget.<\/li>\n<li>Cloudinary stores the original in the <code>altelier-blog<\/code> asset folder.<\/li>\n<li>Cloudinary AI Content Analysis generates a caption.<\/li>\n<li>An upload preset saves the caption as <code>context.custom.alt<\/code>.<\/li>\n<li>The app reads the stored value through the Cloudinary Admin API.<\/li>\n<li>React renders the image with the saved description.<\/li>\n<li>An accessibility library identifies images with missing alt text.<\/li>\n<\/ol>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">flowchart<\/span> <span class=\"hljs-selector-tag\">LR<\/span>\n  <span class=\"hljs-selector-tag\">A<\/span><span class=\"hljs-selector-attr\">&#91;React Upload Widget]<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">B<\/span><span class=\"hljs-selector-attr\">&#91;Cloudinary upload preset]<\/span>\n  <span class=\"hljs-selector-tag\">B<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">C<\/span><span class=\"hljs-selector-attr\">&#91;AI captioning]<\/span>\n  <span class=\"hljs-selector-tag\">C<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">D<\/span><span class=\"hljs-selector-attr\">&#91;Description alt metadata]<\/span>\n  <span class=\"hljs-selector-tag\">C<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">E<\/span><span class=\"hljs-selector-attr\">&#91;autogenerated-alt tag]<\/span>\n  <span class=\"hljs-selector-tag\">D<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">F<\/span><span class=\"hljs-selector-attr\">&#91;Cloudinary Search API]<\/span>\n  <span class=\"hljs-selector-tag\">E<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">F<\/span>\n  <span class=\"hljs-selector-tag\">F<\/span> <span class=\"hljs-selector-tag\">--<\/span>&gt; <span class=\"hljs-selector-tag\">G<\/span><span class=\"hljs-selector-attr\">&#91;Accessible React image]<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The important design choice is that the image and its description share one source of truth. Cloudinary stores both, so another website, mobile app, or content management system can reuse the same reviewed description.<\/p>\n<p>Cloudinary recommends its built-in <code>alt<\/code> <a href=\"https:\/\/cloudinary.com\/documentation\/contextual_metadata\">contextual metadata<\/a> field for centralized text alternatives. Read more in the <a href=\"https:\/\/cloudinary.com\/documentation\/accessible_media_images\">Cloudinary image accessibility guide<\/a>.<\/p>\n<h3>Why Store Alt Text With the Asset?<\/h3>\n<p>An HTML <code>alt<\/code> attribute exists on one rendered page. Cloudinary contextual metadata belongs to the asset itself. If an editor improves the description in Cloudinary, every connected application can request the updated value instead of maintaining separate copies.<\/p>\n<p>Cloudinary doesn\u2019t automatically insert metadata into every HTML page. Your application still decides whether an image is informative, functional, or decorative. It then maps the stored value to the correct <code>alt<\/code> attribute during rendering.<\/p>\n<h2>What You\u2019ll Need<\/h2>\n<p>Before starting, make sure you have:<\/p>\n<ul>\n<li>Node.js 20.19 or newer.<\/li>\n<li>A <a href=\"https:\/\/cloudinary.com\/users\/register_free\">free Cloudinary account<\/a>.<\/li>\n<li>Cloudinary AI Content Analysis enabled for your product environment.<\/li>\n<li>Basic familiarity with React and TypeScript.<\/li>\n<\/ul>\n<p>This project uses React, Vite, shadcn\/ui, the Cloudinary Upload Widget, the Cloudinary React SDK, and the Cloudinary Node.js SDK.<\/p>\n<h2>Create the React Project<\/h2>\n<p>Start with Cloudinary\u2019s React project generator, then initialize <code>shadcn\/ui<\/code>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npx<\/span> <span class=\"hljs-selector-tag\">create-cloudinary-react<\/span>\n<span class=\"hljs-selector-tag\">npx<\/span> <span class=\"hljs-selector-tag\">shadcn<\/span><span class=\"hljs-keyword\">@latest<\/span> init --preset beqDpmlc --template vite --pointer\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Install the project dependencies and start the local app:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install\nnpm run dev\n<\/code><\/span><\/pre>\n<p>The repository also uses Cloudinary\u2019s agent resources during development. You can learn how to connect them in the <a href=\"https:\/\/cloudinary.com\/documentation\/cloudinary_llm_mcp\">Cloudinary MCP servers and skills documentation<\/a>.<\/p>\n<h2>Configure an Unsigned Upload Preset<\/h2>\n<p>An <a href=\"https:\/\/cloudinary.com\/documentation\/dam_admin_upload_presets\">upload preset<\/a> is a saved group of upload rules. The browser sends its preset name with each upload, and Cloudinary applies the configured folder, formats, analysis, and metadata behavior.<\/p>\n<p>Open <strong>Cloudinary Console \u2192 Settings \u2192 Upload \u2192 Upload presets<\/strong>, then select <strong>Add upload preset<\/strong>.<\/p>\n<p>Use these settings in the <strong>General<\/strong> tab:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Setting<\/th>\n<th>Value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Upload preset name<\/td>\n<td><code>altelier_unsigned<\/code><\/td>\n<\/tr>\n<tr>\n<td>Signing mode<\/td>\n<td><code>Unsigned<\/code><\/td>\n<\/tr>\n<tr>\n<td>Asset folder<\/td>\n<td><code>altelier-blog<\/code><\/td>\n<\/tr>\n<tr>\n<td>Overwrite assets with the same public ID<\/td>\n<td>Off<\/td>\n<\/tr>\n<tr>\n<td>Use filename as public ID<\/td>\n<td>Off<\/td>\n<\/tr>\n<tr>\n<td>Unique filename<\/td>\n<td>On<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<h3>How the Preset Controls Each Upload<\/h3>\n<p>The React app sends the preset name instead of sending every upload option from the browser. Cloudinary looks up the preset and enforces its settings. This keeps folder placement, captioning, file restrictions, and metadata behavior consistent across uploads.<\/p>\n<p>The demo uses an unsigned preset because the browser uploads directly through the <a href=\"https:\/\/cloudinary.com\/documentation\/upload_widget\">Cloudinary Upload Widget<\/a>. The browser needs only the cloud name and preset name. It never receives the Cloudinary API secret.<\/p>\n<h3>When to Use Signed Uploads Instead<\/h3>\n<p>An unsigned preset works well for a public demo with strict format and size limits. Its name remains visible in browser code, so another person could reuse it to upload unwanted files.<\/p>\n<p>Use a signed upload when your application must:<\/p>\n<ul>\n<li>Require an authenticated account before upload.<\/li>\n<li>Set sensitive upload options on the server.<\/li>\n<li>Apply per-user folder or access rules.<\/li>\n<li>Prevent anyone outside your application from reusing the upload configuration.<\/li>\n<\/ul>\n<p><a href=\"https:\/\/cloudinary.com\/documentation\/upload_widget#signed_uploads\">Signed uploads<\/a> require a server endpoint that creates a short-lived signature. The rest of this tutorial uses an unsigned preset to keep the accessibility workflow focused.<\/p>\n<p>In <strong>Optimize and Deliver<\/strong>, set <strong>Allowed formats<\/strong> to:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">jpg,jpeg,png,webp,avif\n<\/code><\/span><\/pre>\n<p>Leave <strong>Incoming transformation<\/strong>, <strong>Eager transformations<\/strong>, and <strong>Format<\/strong> blank. This preserves the original upload. Cloudinary can create optimized delivery variants when a browser requests them.<\/p>\n<h2>Enable Cloudinary AI Captioning<\/h2>\n<p>Open the preset\u2019s <strong>Addons<\/strong> tab. Under <strong>AI media processing<\/strong>, find <strong>Cloudinary AI Content Analysis<\/strong> and enable <strong>Add AI captioning to your image<\/strong>.<\/p>\n<p>If the option is unavailable, subscribe to the add-on from your Cloudinary account first. The <a href=\"https:\/\/cloudinary.com\/documentation\/cloudinary_ai_content_analysis_addon\">Cloudinary AI Content Analysis documentation<\/a> explains registration and supported analysis features.<\/p>\n<p>During upload, the preset requests AI captioning from Cloudinary AI Content Analysis. The service analyzes the visible subjects, setting, and actions, then adds a natural-language caption to the upload information. Cloudinary\u2019s <a href=\"https:\/\/cloudinary.com\/documentation\/dam_add_ons#cloudinary_ai_content_analysis\">Digital Asset Management add-on guide<\/a> explains where generated captions appear in the Media Library.<\/p>\n<p>Captioning during upload works best when new assets should enter your library with a first draft description. If you already have a large Media Library, use an API update or a <a href=\"https:\/\/cloudinary.com\/documentation\/accessible_media_images#alternative_ways_to_invoke_ai_based_image_captioning\">Cloudinary MediaFlows captioning workflow<\/a> to process existing assets instead.<\/p>\n<p>AI captions are a starting point, not a replacement for editorial review. A useful alt description depends on the image\u2019s purpose and surrounding content. Mark decorative images with an empty <code>alt=&quot;&quot;<\/code> instead of describing them.<\/p>\n<h3>How a Caption Becomes Useful Alt Text<\/h3>\n<p>A caption describes visible content. Alt text communicates the image\u2019s purpose in its current context. Those goals overlap, but they are not identical.<\/p>\n<p>For example, AI might produce \u201cA person holding a red umbrella outside a train station.\u201d That description can work in a travel article. If the image is a link to weather alerts, the useful alternative may be \u201cView current weather alerts.\u201d Treat the generated caption as a draft that an editor can keep, shorten, or replace.<\/p>\n<h2>Store the Generated Caption as Alt Metadata<\/h2>\n<p>Generating a caption is only half of the workflow. The description must remain available after the upload response disappears.<\/p>\n<p>Open the preset\u2019s <strong>Advanced<\/strong> tab and add this <strong>On success script<\/strong>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">if<\/span> (e.upload_info?.info?.detection?.captioning?.data?.caption) {\n  current_asset.update({\n    <span class=\"hljs-attr\">tags<\/span>: &#91;<span class=\"hljs-string\">'autogenerated-alt'<\/span>],\n    <span class=\"hljs-attr\">context<\/span>: {\n      <span class=\"hljs-attr\">alt<\/span>: e.upload_info.info.detection.captioning.data.caption\n    }\n  })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This script performs two jobs:<\/p>\n<ul>\n<li>It writes the caption to Cloudinary\u2019s built-in <strong>Description (alt)<\/strong> field.<\/li>\n<li>It adds the <code>autogenerated-alt<\/code> tag for filtering and review.<\/li>\n<\/ul>\n<h3>How the Success Script Works<\/h3>\n<p><code>e.upload_info<\/code> contains the completed upload response. The script follows the response path to <code>captioning.data.caption<\/code>, checks that text exists, and calls <code>current_asset.update()<\/code>. Cloudinary then stores the description and tag on the same asset.<\/p>\n<h3>Why Persistence Matters<\/h3>\n<p>The Upload Widget callback exists only in the current browser session. Refreshing the page removes that JavaScript state. Saving the caption as contextual metadata makes it available through the Media Library, Admin API, Search API, and future applications.<\/p>\n<p>The condition also protects the workflow. Cloudinary adds the <code>autogenerated-alt<\/code> tag only when captioning returns text. The tag records how the description was created, not whether a person approved it. Editors can use it as a review queue instead of treating every upload as successfully reviewed.<\/p>\n<p>Save the preset. The complete console configuration is recorded in the project\u2019s <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/docs\/ALT_TEXT_WORKFLOW.md\">alt-text workflow guide<\/a>.<\/p>\n<h2>Configure Browser and Server Credentials<\/h2>\n<p>Create <code>.env<\/code> from <code>.env.example<\/code>, then add these values:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name_here\nVITE_CLOUDINARY_UPLOAD_PRESET=altelier_unsigned\n\nCLOUDINARY_CLOUD_NAME=your_cloud_name_here\nCLOUDINARY_API_KEY=your_api_key_here\nCLOUDINARY_API_SECRET=your_api_secret_here\nCLOUDINARY_ASSET_FOLDER=altelier-blog\nALTELIER_DELETE_TOKEN=your_long_random_delete_token_here\n<\/code><\/span><\/pre>\n<p>The two <code>VITE_<\/code> values are browser-safe. The Upload Widget needs the cloud name and unsigned preset name.<\/p>\n<p>The API key and secret authenticate Admin API requests. Keep them on the server, exclude <code>.env<\/code> from Git, and never prefix the secret with <code>VITE_<\/code>. <code>ALTELIER_DELETE_TOKEN<\/code> protects the demo\u2019s destructive gallery action. It is <strong>not<\/strong> a Cloudinary credential.<\/p>\n<p>The <a href=\"https:\/\/cloudinary.com\/documentation\/node_integration#configuration\">Cloudinary Node.js SDK guide<\/a> covers both environment-based and code-based configuration.<\/p>\n<h3>Why the App Needs a Browser and Server Boundary<\/h3>\n<p>Uploading with an unsigned preset is a browser operation. Searching private asset metadata, reading folder contents, and deleting assets are trusted server operations. Vite includes every <code>VITE_<\/code> variable in the browser bundle, which means visitors can inspect those values.<\/p>\n<p>Keep <code>CLOUDINARY_API_SECRET<\/code> on the Express server or Vercel Function. React calls your <code>\/api<\/code> routes, and those routes call Cloudinary. This boundary lets the interface use Cloudinary management features without exposing account credentials.<\/p>\n<h2>Open the Cloudinary Upload Widget<\/h2>\n<p>The Upload Widget accepts local files, camera captures, and remote URLs. The main configuration needs the cloud name and preset:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">window<\/span><span class=\"hljs-selector-class\">.cloudinary<\/span><span class=\"hljs-selector-class\">.createUploadWidget<\/span>(\n  {\n    <span class=\"hljs-attribute\">cloudName<\/span>: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,\n    uploadPreset: import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET,\n    sources: &#91;<span class=\"hljs-string\">'local'<\/span>, <span class=\"hljs-string\">'camera'<\/span>, <span class=\"hljs-string\">'url'<\/span>],\n    resourceType: <span class=\"hljs-string\">'image'<\/span>,\n    maxImageFileSize: <span class=\"hljs-number\">10<\/span>_000_000,\n    multiple: false,\n  },\n  <span class=\"hljs-selector-tag\">handleWidgetEvent<\/span>,\n)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The widget emits events for opening, queuing, uploading, success, and failure. Handle only the <code>success<\/code> event as a completed asset. Its response contains the <code>public_id<\/code>, dimensions, format, delivery URL, and captioning information.<\/p>\n<p>Altelier keeps the <code>public_id<\/code> because it is the stable input for Cloudinary delivery transformations. The interface then requests the asset metadata from the server. This second request confirms that Cloudinary persisted both the description and tag before the app reports success.<\/p>\n<blockquote>\n<p>See the complete UploadWidget.tsx implementation: (<a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/src\/cloudinary\/UploadWidget.tsx\">https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/src\/cloudinary\/UploadWidget.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Read the Image and Alt Text From Cloudinary<\/h2>\n<p>Browser code must not call the authenticated Admin API. Altelier uses a small Express API with the Cloudinary Node.js SDK.<\/p>\n<p>The server searches only the <code>altelier-blog<\/code> folder and requests the fields needed by the interface:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> cloudinary.search\n  .expression(\n    <span class=\"hljs-string\">'resource_type:image AND asset_folder=\"altelier-blog\"'<\/span>,\n  )\n  .with_field(<span class=\"hljs-string\">'context'<\/span>)\n  .with_field(<span class=\"hljs-string\">'tags'<\/span>)\n  .sort_by(<span class=\"hljs-string\">'created_at'<\/span>, <span class=\"hljs-string\">'desc'<\/span>)\n  .max_results(<span class=\"hljs-number\">100<\/span>)\n  .execute()\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Cloudinary\u2019s <a href=\"https:\/\/cloudinary.com\/documentation\/search_method\">Search API<\/a> supports folder, tag, contextual metadata, format, dimension, and date expressions. Altelier sorts by creation time so the newest four images appear first.<\/p>\n<h3>When to Use Search API or the Resource Method<\/h3>\n<p>Use the Search API when you need a collection, such as every image in a folder or every asset missing metadata. Use the Admin API <code>resource<\/code> method when you already know one <code>public_id<\/code> and want to verify that asset.<\/p>\n<p>Altelier uses both approaches. The gallery searches the folder, while the post-upload check requests one asset. Both calls run on the server because they require authenticated Cloudinary management access.<\/p>\n<p>The upload response and Search API can expose contextual metadata in slightly different shapes. The server accepts both:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getStoredAlt<\/span>(<span class=\"hljs-params\">resource: SearchResource<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    resource.context?.custom?.alt?.trim() ||\n    resource.context?.alt?.trim() ||\n    <span class=\"hljs-literal\">null<\/span>\n  )\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p>See all API routes, folder checks, and deletion safeguards: <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/server\/app.ts\">https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/server\/app.ts<\/a><\/p>\n<\/blockquote>\n<h2>Render an Accessible, Responsive Umage<\/h2>\n<p>The Cloudinary React SDK builds a delivery image from its <code>public_id<\/code>. <code>c_fit<\/code> keeps the full image visible inside the requested bounds. <code>f_auto<\/code> selects a format supported by the browser, while <code>q_auto<\/code> balances visual quality and file size.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">const image = cld\n  .image(asset.publicId)\n  .resize(fit().width(1600).height(1200))\n  .delivery(format(auto()))\n  .delivery(quality(autoQuality()))\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">AdvancedImage<\/span>\n  <span class=\"hljs-attr\">cldImg<\/span>=<span class=\"hljs-string\">{image}<\/span>\n  <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">{asset.alt}<\/span>\n  <span class=\"hljs-attr\">plugins<\/span>=<span class=\"hljs-string\">{&#91;<\/span>\n    <span class=\"hljs-attr\">responsive<\/span>(),\n    <span class=\"hljs-attr\">placeholder<\/span>({ <span class=\"hljs-attr\">mode:<\/span> '<span class=\"hljs-attr\">blur<\/span>' }),\n    <span class=\"hljs-attr\">lazyload<\/span>(),\n  ]}\n\/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The <code>responsive()<\/code> plugin requests an image size suited to the rendered width. The blurred placeholder improves the loading state, and explicit dimensions reduce layout movement.<\/p>\n<blockquote>\n<p>See the <a href=\"https:\/\/cloudinary.com\/documentation\/react_image_transformations\">React image transformation guide<\/a> and <a href=\"https:\/\/cloudinary.com\/documentation\/image_optimization\">Cloudinary image optimization guide<\/a>.<\/p>\n<\/blockquote>\n<h3>Why These Delivery Options Work Together<\/h3>\n<p><code>c_fit<\/code> resizes the image inside a maximum width and height without removing content. Use it when the complete image matters, such as editorial graphics, posters, or screenshots. Use a crop mode such as <code>c_fill<\/code> only when filling the frame matters more than preserving every edge.<\/p>\n<p><code>f_auto<\/code> reads the browser\u2019s supported formats and can deliver formats such as AVIF or WebP. <code>q_auto<\/code> analyzes the image and selects an encoding quality that balances file size with visual quality. These transformations affect the delivered version, while the original asset remains available in Cloudinary.<\/p>\n<p>Altelier renders a blurred decorative copy behind images that do not fill the showcase frame. The foreground uses the stored description. The decorative backdrop uses <code>alt=&quot;&quot;<\/code> and <code>aria-hidden=&quot;true&quot;<\/code>.<\/p>\n<blockquote>\n<p>View the full gallery component: <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/src\/components\/asset-library.tsx\">https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/src\/components\/asset-library.tsx<\/a><\/p>\n<\/blockquote>\n<blockquote>\n<p>View Portable React, HTML, Vue, and content management system examples: <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/src\/components\/integration-code-panel.tsx\">https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/src\/components\/integration-code-panel.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Verify the Complete Workflow<\/h2>\n<p>Test the result from upload to rendered HTML:<\/p>\n<ol>\n<li>Run <code>npm run dev<\/code>.<\/li>\n<li>Open the upload studio.<\/li>\n<li>Upload an image with a clear subject.<\/li>\n<li>Wait for <strong>Alt text saved in Cloudinary<\/strong>.<\/li>\n<li>Open the asset in Cloudinary Media Library.<\/li>\n<li>Confirm <strong>Description (alt)<\/strong> contains the caption.<\/li>\n<li>Confirm <strong>Tags<\/strong> contains <code>autogenerated-alt<\/code>.<\/li>\n<li>Inspect the rendered image and check its <code>alt<\/code> attribute.<\/li>\n<li>Review the asset under <strong>Accessibility library<\/strong>.<\/li>\n<\/ol>\n<p>These checks cover three different layers. The Upload Widget confirms that Cloudinary accepted the file. The Media Library confirms that captioning and the success script stored metadata. Browser inspection confirms that React used the stored description in the final HTML.<\/p>\n<p>If the image uploads without a description, confirm that the app uses the same preset you edited. Also confirm that <strong>Add AI captioning to your image<\/strong> and the <strong>On success script<\/strong> are saved.<\/p>\n<h2>Deploy the Vite App and API<\/h2>\n<p>The repository includes a Vercel Function for <code>\/api\/*<\/code> and a Vite production build. Add all variables from <code>.env.example<\/code> to the Vercel project before deploying. Do not add <code>PORT<\/code>, because Vercel manages the function runtime.<\/p>\n<p>Vite reads <code>VITE_<\/code> variables while building the frontend. Vercel Functions read server variables while handling API requests. Add both groups before triggering a production deployment, then redeploy whenever you change a frontend variable.<\/p>\n<p>Run these checks before each deployment:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm run lint\nnpm run build\n<\/code><\/span><\/pre>\n<blockquote>\n<p>Follow the project\u2019s <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\/blob\/main\/docs\/VERCEL_DEPLOYMENT.md\">Vercel deployment guide<\/a> for environment scopes and production checks.<\/p>\n<\/blockquote>\n<h2>Build Accessibility Into the Visual Media Lifecycle<\/h2>\n<p>Alt text should not be an afterthought added to one page. Store the description with the image, make missing descriptions visible, and review AI output before publication.<\/p>\n<p>Cloudinary can manage the upload, caption generation, metadata, responsive transformation, and optimized delivery. React only needs to render the right asset with the right description.<\/p>\n<p>Explore the <a href=\"https:\/\/github.com\/musebe\/react-cloudinary-alt-text\">Altelier source repository<\/a>, try the <a href=\"https:\/\/react-cloudinary-alt-text.vercel.app\/\">live accessibility studio<\/a>, and compare this workflow with Cloudinary\u2019s original article about <a href=\"https:\/\/cloudinary.com\/blog\/making_media_accessible_how_to_automatically_generate_alt_text_for_images\">automatically generating alt text for images<\/a>. <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Sign up<\/a> for a free Cloudinary account to get started.<\/p>\n<\/div>\n\n\n<style>\n.faqs {\n    padding: 30px 60px;\n    margin-top: 40px;\n    background: var(--color-background-offset);\n    border-radius: 20px;\n}\n\n#frequently_asked_questions {\n    margin-bottom: 20px;\n}\n\n.question {\n    margin-bottom: 12px;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n}\n\n.question summary {\n    position: relative;\n    padding: 18px 40px 18px 0;\n    cursor: pointer;\n    font-weight: 700;\n    list-style: none;\n}\n\n.question summary::-webkit-details-marker {\n    display: none;\n}\n\n.question summary::after {\n    content: \"+\";\n    position: absolute;\n    top: 50%;\n    right: 0;\n    transform: translateY(-50%);\n    font-size: 24px;\n    font-weight: 400;\n    line-height: 1;\n}\n\n.question[open] summary::after {\n    content: \"\u2212\";\n}\n\n.answer {\n    padding: 0 40px 20px 0;\n}\n<\/style>\n\n<div class=\"faqs\">\n\n<h2 id=\"frequently_asked_questions\">Frequently Asked Questions<\/h2>\n\n<details class=\"question\">\n    <summary>How does Cloudinary generate alt text for an uploaded image?<\/summary>\n    <div class=\"answer\">Cloudinary AI Content Analysis examines the image during upload and returns a caption. An upload preset can then save that caption in the asset&#8217;s `alt` contextual metadata field.<\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>Where should generated alt text be stored?<\/summary>\n    <div class=\"answer\">Store it with the Cloudinary asset as contextual or structured metadata. Central storage lets websites, apps, and content tools read the same reviewed description.<\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>Does AI-generated alt text still need human review?<\/summary>\n    <div class=\"answer\">Yes. AI can describe visible content, but an editor understands why the image appears in a specific article. Review descriptions for accuracy, context, names, and unnecessary detail.<\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>Can React read Cloudinary alt metadata directly?<\/summary>\n    <div class=\"answer\">Use a trusted server to call the Cloudinary Admin API, then return the required metadata to React. Never expose the Cloudinary API secret in a frontend bundle.<\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>What happens when an image has no generated caption?<\/summary>\n    <div class=\"answer\">The app marks it as missing alt text and excludes the `autogenerated-alt` tag. An editor can then write or approve a description before publication.<\/div>\n<\/details>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Frequently Asked Questions How does Cloudinary generate alt text for an uploaded image? Cloudinary AI Content Analysis examines the image during upload and returns a caption. An upload preset can then save that caption in the asset&#8217;s `alt` contextual metadata field. Where should generated alt text be stored? Store it with the Cloudinary asset as [&hellip;]<\/p>\n","protected":false},"author":87,"featured_media":40567,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[337,336,332,246,373],"class_list":["post-40565","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-accessibility","tag-ai","tag-api","tag-react","tag-upload"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v28.3 (Yoast SEO v28.3) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI<\/title>\n<meta name=\"description\" content=\"Learn how to build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image&#039;s HTML `alt` attribute.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI\" \/>\n<meta property=\"og:description\" content=\"Learn how to build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image&#039;s HTML `alt` attribute.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-22T14:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#\\\/schema\\\/person\\\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI\",\"datePublished\":\"2026-10-22T14:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image\"},\"wordCount\":192,\"publisher\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1789762859\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA\",\"keywords\":[\"Accessibility\",\"AI\",\"API\",\"React\",\"Upload\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2026\",\"copyrightHolder\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image\",\"url\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image\",\"name\":\"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1789762859\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA\",\"datePublished\":\"2026-10-22T14:00:00+00:00\",\"description\":\"Learn how to build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image's HTML `alt` attribute.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#primaryimage\",\"url\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1789762859\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA\",\"contentUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1789762859\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\\\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/auto-generate-alt-text-uploaded-image#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1649718331\\\/Web_Assets\\\/blog\\\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\\\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1649718331\\\/Web_Assets\\\/blog\\\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\\\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#\\\/schema\\\/person\\\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI","description":"Learn how to build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image's HTML `alt` attribute.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","og_locale":"en_US","og_type":"article","og_title":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI","og_description":"Learn how to build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image's HTML `alt` attribute.","og_url":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","og_site_name":"Cloudinary Blog","article_published_time":"2026-10-22T14:00:00+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA","type":"image\/png"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI","datePublished":"2026-10-22T14:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image"},"wordCount":192,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA","keywords":["Accessibility","AI","API","React","Upload"],"inLanguage":"en-US","copyrightYear":"2026","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","url":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","name":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA","datePublished":"2026-10-22T14:00:00+00:00","description":"Learn how to build a React app that uploads an image to Cloudinary, generates a description with Cloudinary AI, stores that description with the asset, and uses it as the image's HTML `alt` attribute.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"parsely":{"version":"1.1.0","canonical_url":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","meta":{"@context":"https:\/\/schema.org","@type":"NewsArticle","headline":"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI","url":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image","mainEntityOfPage":{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA&w=150&h=150&crop=1","image":{"@type":"ImageObject","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA"},"articleSection":"Uncategorized","author":[{"@type":"Person","name":"melindapham"}],"creator":["melindapham"],"publisher":{"@type":"Organization","name":"Cloudinary Blog","logo":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA"},"keywords":["accessibility","ai","api","react","upload"],"dateCreated":"2026-10-22T14:00:00Z","datePublished":"2026-10-22T14:00:00Z","dateModified":"2026-10-22T14:00:00Z"},"rendered":"<meta name=\"parsely-title\" content=\"Auto-Generate Alt Text for Every Uploaded Image: How to Build an Accessible React App With Cloudinary AI\" \/>\n<meta name=\"parsely-link\" content=\"https:\/\/cloudinary.com\/blog\/auto-generate-alt-text-uploaded-image\" \/>\n<meta name=\"parsely-type\" content=\"post\" \/>\n<meta name=\"parsely-image-url\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA&w=150&amp;h=150&amp;crop=1\" \/>\n<meta name=\"parsely-pub-date\" content=\"2026-10-22T14:00:00Z\" \/>\n<meta name=\"parsely-section\" content=\"Uncategorized\" \/>\n<meta name=\"parsely-tags\" content=\"accessibility,ai,api,react,upload\" \/>\n<meta name=\"parsely-author\" content=\"melindapham\" \/>","tracker_url":"https:\/\/cdn.parsely.com\/keys\/cloudinary.com\/p.js"},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1789762859\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI\/Blog_Auto-Generate_Alt_Text_for_Every_Image_You_Upload_Build_an_Accessible_React_App_with_Cloudinary_AI.png?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/40565","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=40565"}],"version-history":[{"count":3,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/40565\/revisions"}],"predecessor-version":[{"id":40569,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/40565\/revisions\/40569"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/40567"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=40565"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=40565"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=40565"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}