cloudinary-react

Installation
SKILL.md

Cloudinary React Skill

When to Use

  • When a user is building or debugging Cloudinary in a React app (Vite, Create React App, Parcel, etc.).
  • When implementing or fixing: Upload Widget, AdvancedImage/AdvancedVideo, transformations, overlays, image galleries, video player, or signed/unsigned uploads.
  • When the user sees errors like "createUploadWidget is not a function", wrong imports from @cloudinary/url-gen, upload preset issues, or video player DOM errors.

Quick Start

Most common operations:

  1. Setup: Create config file with cld instance (see Project setup section)
  2. Display image: const img = cld.image('id').resize(fill().width(800)); <AdvancedImage cldImg={img} />
  3. Upload Widget: Script in index.html + poll for createUploadWidget in useEffect
  4. Image overlay: Use source(text(...)) or source(image(...)) - see Import reference table for exact paths
  5. Signed uploads: See references/signed-uploads.md
  6. Troubleshooting: See references/troubleshooting.md

For TypeScript: See references/typescript-patterns.md For Video Player: See references/video-player.md

Instructions

When helping with Cloudinary in React, follow the patterns and rules below. Use the exact import paths and code patterns specified; do not guess subpaths or invent APIs.


Cloudinary React SDK Patterns & Common Errors

Scope: These rules apply to React (web) with the browser Upload Widget. The default is Vite (create-cloudinary-react uses Vite). They also work with other bundlers (Create React App, Next.js, Parcel, etc.): only how you read env vars changes; see "Other bundlers (non-Vite)" below. Rules-only users: see "Project setup (rules-only / without CLI)" for the reusable Cloudinary instance, env, Upload Widget (unsigned/signed), and video player. For React Native uploads (including signed upload), see: https://cloudinary.com/documentation/react_native_image_and_video_upload#signed_upload — same "never expose secret, generate signature on backend" principle, but React Native uses the upload() method and backend SDKs differently.

Official Documentation

For complete API references and all configuration options, consult the Cloudinary documentation via llms.txt: https://cloudinary.com/documentation/llms.txt

This skill provides React patterns and common errors. For detailed API options (e.g., all Upload Widget config properties, Video Player API methods), use the documentation markdown files available through llms.txt.

Key References

Golden rule for @cloudinary/url-gen: When generating overlay or transformation code, use only the "Import reference" table and "Canonical overlay block" in these rules. Copy those import paths and usage patterns exactly; do not guess or derive paths (e.g. text and image are from qualifiers/source, not actions/overlay; Position is from qualifiers/position, not positioning/Position).


📋 PATTERNS (How to Do It Right)

Project setup (rules-only / without CLI)

If the user is not using the create-cloudinary-react CLI and only has these rules, generate the following so they get correct config, env, and widget setup.

1. Environment (.env)
Create a .env file in the project root with Vite prefix (required for client access):

  • VITE_CLOUDINARY_CLOUD_NAME_=my_cloud (required — use your actual cloud name, never the literal string your_cloud_name which causes 401)
  • VITE_CLOUDINARY_UPLOAD_PRESET=my_preset (optional; required for unsigned upload widget — use your actual preset name)
  • Restart the dev server after adding or changing .env. Use import.meta.env.VITE_* in code, not process.env.
  • If env var still empty in browser after restart: Vite may cache the old value. Clear node_modules/.vite/, restart dev server, and do a hard refresh (Cmd+Shift+R / Ctrl+Shift+F5). If still empty, see "Vite env not reaching client" in Common Errors.

2. Reusable Cloudinary instance (config)
Create a config file (e.g. src/cloudinary/config.ts) so the rest of the app can use a single cld instance:

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

const cloudName = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME;
if (!cloudName) {
  throw new Error('VITE_CLOUDINARY_CLOUD_NAME is not set. Add it to .env with the VITE_ prefix.');
}

export const cld = new Cloudinary({ cloud: { cloudName } });
export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || '';
  • Use this pattern for the reusable instance. Everywhere else: import { cld } from './cloudinary/config' (or the path the user chose) and call cld.image(publicId) / cld.video(publicId).

3. Upload Widget (unsigned, from scratch)

Strict pattern (always follow this exactly):

  1. Script in index.html (required): Add <script src="https://upload-widget.cloudinary.com/global/all.js" async></script> to index.html. Do not rely only on dynamic script injection from React — it's fragile.
  2. Poll in useEffect (required): In useEffect, poll with setInterval (e.g. every 100ms) until typeof window.cloudinary?.createUploadWidget === 'function'. Only then create the widget. A single check (even in onload) is not reliable because window.cloudinary can exist before createUploadWidget is attached.
  3. Add a timeout: Set a timeout (e.g. 10 seconds) to stop polling and show an error if the script never loads. Clear both interval and timeout in cleanup.
  4. Create widget once: When createUploadWidget is available, create the widget and store it in a ref. Clear the interval and timeout. Pass options: { cloudName, uploadPreset, sources: ['local', 'camera', 'url'], multiple: false }.
  5. Open on click: Attach a click listener to a button that calls widgetRef.current?.open(). Remove the listener in useEffect cleanup.

Do NOT: Check only window.cloudinary (not enough); do a single check in onload (unreliable); skip the script in index.html; poll forever without a timeout.

  • Signed uploads: Do not use only uploadPreset; use the pattern under "Secure (Signed) Uploads" (uploadSignature as function, fetch api_key, server includes upload_preset in signature).

4. Video player

  • Use imperative video element only (create with document.createElement, append to container ref, pass to videoPlayer). See "Cloudinary Video Player (The Player)" for the full pattern.

5. Summary for rules-only users

  • Env: Use your bundler's client env prefix and access (Vite: VITE_ + import.meta.env.VITE_*; see "Other bundlers" if not Vite).
  • Reusable instance: One config file that creates and exports cld (and optionally uploadPreset) from @cloudinary/url-gen; use it everywhere.
  • Upload widget: Script in index.html (required); in useEffect, poll until createUploadWidget is a function, then create widget once and store in ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend.
  • Video player: Imperative video element (createElement, append to container ref, pass to videoPlayer); dispose + removeChild in cleanup; fall back to AdvancedVideo if init fails.

If the user is not using Vite: Use their bundler's client env prefix and access in the config file and everywhere you read env. Examples: Create React App → REACT_APP_CLOUDINARY_CLOUD_NAME, process.env.REACT_APP_CLOUDINARY_CLOUD_NAME; Next.js (client) → NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME. The rest (cld instance, widget options, video player) is the same.

Environment Variables

  • Default: Vite — Vite requires VITE_ prefix; use import.meta.env.VITE_CLOUDINARY_CLOUD_NAME (not process.env). Restart dev server after changing .env.
  • ✅ CORRECT (Vite): VITE_CLOUDINARY_CLOUD_NAME=mycloud in .env; import.meta.env.VITE_CLOUDINARY_CLOUD_NAME
  • WRONG: VITE_CLOUDINARY_CLOUD_NAME=your_cloud_nameNever use the literal placeholder your_cloud_name; it causes 401 errors. Use your actual cloud name from the Cloudinary dashboard.
  • ⚠️ Vite env not reaching client: If import.meta.env.VITE_CLOUDINARY_CLOUD_NAME is still undefined in the browser after restarting:
    1. Clear Vite cache: rm -rf node_modules/.vite/ (or delete node_modules/.vite folder)
    2. Restart dev server: npm run dev
    3. Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+F5 (Windows/Linux)
    4. If still empty, use a static config workaround (create a cloudinaryConfig.ts with a hardcoded cloud name for dev, then switch back to env later) and see Common Errors → "VITE_ prefix required or env var is undefined"

Other bundlers (non-Vite)

  • Only the env access changes. All other patterns (reusable cld, Upload Widget, Video Player, overlays, signed uploads) are bundler-agnostic.
  • Create React App: Prefix REACT_APP_; access process.env.REACT_APP_CLOUDINARY_CLOUD_NAME, process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET. Restart dev server after .env changes.
  • Next.js (client): Prefix NEXT_PUBLIC_ for client; access process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, etc. Server-side can use process.env.CLOUDINARY_* without NEXT_PUBLIC_.
  • Parcel / other: Check the bundler's docs for "exposing environment variables to the client" (often a prefix or allowlist). Use that prefix and the documented access (e.g. process.env.*).
  • Config file: In src/cloudinary/config.ts (or equivalent), read cloud name and upload preset using the user's bundler env API (e.g. for CRA: process.env.REACT_APP_CLOUDINARY_CLOUD_NAME). Same new Cloudinary({ cloud: { cloudName } }) and exports; only the env read line changes.

Upload Presets

  • Unsigned = client-only uploads (no backend). Signed = backend required, more secure. See "Signed vs unsigned uploads" below for when to use which.
  • ✅ Create unsigned upload preset (for simple client uploads): https://console.cloudinary.com/app/settings/upload/presets
  • ✅ Set preset in .env: VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name
  • ✅ Use in code: import { uploadPreset } from './cloudinary/config'
  • ⚠️ If upload preset is missing, the Upload Widget will show an error message
  • ⚠️ Upload presets must be set to "Unsigned" mode for client-side usage (no API key/secret needed)
  • When unsigned upload fails: First check that the user configured their upload preset:
    1. Is VITE_CLOUDINARY_UPLOAD_PRESET set in .env? (must match preset name exactly)
    2. Does the preset exist in the dashboard under Settings → Upload → Upload presets?
    3. Is the preset set to Unsigned (not Signed)?
    4. Was the dev server restarted after adding/updating .env?

Installing Cloudinary packages

  • Install the latest: When adding Cloudinary packages, use npm install <package> with no version so npm installs the latest compatible version (e.g. npm install cloudinary-video-player). In package.json use a caret range (e.g. "cloudinary-video-player": "^1.0.0") so future installs get the latest compatible. Do not pin to an exact version unless you have verified it exists on npm.
  • Package names only: Use only these names: @cloudinary/react, @cloudinary/url-gen, cloudinary-video-player (standalone player), cloudinary (Node server-side only). Do not invent names (e.g. no @cloudinary/video-player).
  • WRONG: npm install cloudinary-video-player@1.2.3 or "cloudinary-video-player": "1.2.3" (exact pin) — versions may not exist and break installs.
  • Correct: npm install cloudinary-video-player (no version) or in package.json: "cloudinary-video-player": "^1.0.0" (caret = latest compatible).

Import Patterns

  • ✅ Import Cloudinary instance: import { cld } from './cloudinary/config'
  • ✅ Import components: import { AdvancedImage, AdvancedVideo } from '@cloudinary/react'
  • ✅ Import plugins: import { responsive, lazyload, placeholder } from '@cloudinary/react'
  • For transformations and overlays, use only the exact paths in "Import reference: @cloudinary/url-gen" and the "Canonical overlay block" below. Do not guess subpaths (e.g. text and image are from qualifiers/source, not actions/overlay).

Import reference: @cloudinary/url-gen (use these exact paths only)

Rule: Do not invent or guess import paths for @cloudinary/url-gen. Use only the paths in the table and canonical block below. Copy the import statements exactly; do not derive paths (e.g. @cloudinary/url-gen/overlay exports only sourcetext and image are from qualifiers/source; Position is from qualifiers/position, not positioning/Position). Wrong paths cause "module not found" or "does not exist".

Purpose Exact import
Cloudinary instance (config) import { Cloudinary } from '@cloudinary/url-gen';
Resize (fill) import { fill } from '@cloudinary/url-gen/actions/resize';
Resize (scale, for overlays) import { scale } from '@cloudinary/url-gen/actions/resize';
Delivery format/quality import { format, quality } from '@cloudinary/url-gen/actions/delivery';
Format qualifier (auto) import { auto } from '@cloudinary/url-gen/qualifiers/format';
Quality qualifier (auto) import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality';
Effects (e.g. blur) import { blur } from '@cloudinary/url-gen/actions/effect';
Overlay source import { source } from '@cloudinary/url-gen/actions/overlay';
Overlay text / image (source types) import { text, image } from '@cloudinary/url-gen/qualifiers/source';
Overlay image transformation import { Transformation } from '@cloudinary/url-gen/transformation/Transformation';
Position (overlay) import { Position } from '@cloudinary/url-gen/qualifiers/position';
Gravity/compass import { compass } from '@cloudinary/url-gen/qualifiers/gravity';
Text style (overlay) import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';
Types import type { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';

Canonical overlay block (copy these imports and patterns exactly):

// Overlay imports — text/image from qualifiers/source, NOT actions/overlay
import { source } from '@cloudinary/url-gen/actions/overlay';
import { text, image } from '@cloudinary/url-gen/qualifiers/source';
import { Position } from '@cloudinary/url-gen/qualifiers/position';
import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';
import { compass } from '@cloudinary/url-gen/qualifiers/gravity';
import { Transformation } from '@cloudinary/url-gen/transformation/Transformation';
import { scale } from '@cloudinary/url-gen/actions/resize';

// Text overlay (compass with underscores: 'south_east', 'center')
cld.image('id').overlay(
  source(text('Hello', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white'))
    .position(new Position().gravity(compass('center')))
);

// Image overlay (logo/image with resize)
cld.image('id').overlay(
  source(image('logo').transformation(new Transformation().resize(scale().width(100))))
    .position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))
);
  • Components (AdvancedImage, AdvancedVideo, plugins) come from @cloudinary/react, not from @cloudinary/url-gen.
  • Transformation actions and qualifiers (resize, delivery, effect, overlay, etc.) come from @cloudinary/url-gen/actions/* and @cloudinary/url-gen/qualifiers/* with the exact subpaths above.
  • If an import fails, verify the package version (@cloudinary/url-gen in package.json) and the Cloudinary URL-Gen SDK docs or Transformation Builder reference.

Creating Image & Video Instances

  • ✅ Create image instance: const img = cld.image(publicId)
  • ✅ Create video instance: const video = cld.video(publicId) (same pattern as images)
  • ✅ Public ID format: Use forward slashes for folders (e.g., 'folder/subfolder/image')
  • ✅ Public IDs are case-sensitive and should not include file extensions
  • Sample assets: Cloudinary may provide sample assets under samples/. Assume they might not exist (users can delete them); always handle load errors and provide fallbacks (see Image gallery). When they exist, use them for examples and demos instead of requiring uploads first.
  • Sample public IDs that may be available (use for galleries, demos; handle onError if missing):
    • Images: samples/cloudinary-icon, samples/two-ladies, samples/food/spices, samples/landscapes/beach-boat, samples/bike, samples/landscapes/girl-urban-view, samples/animals/reindeer, samples/food/pot-mussels
    • Video: samples/elephants
  • Default / most reliable: Start with samples/cloudinary-icon for a single image; use the list above for galleries or variety. Prefer uploaded assets when the user has them.
  • ✅ Examples:
    const displayImage = cld.image('samples/cloudinary-icon');
    const displayVideo = cld.video('samples/elephants');
    // Gallery: e.g. ['samples/bike', 'samples/landscapes/beach-boat', 'samples/food/spices', ...]
    

Transformation Patterns

Image Transformations

  • ✅ Chain transformations on image instance:
    const img = cld.image('id')
      .resize(fill().width(800).height(600))
      .effect(blur(800))
      .delivery(format(auto()))
      .delivery(quality(autoQuality()));
    
  • ✅ Pass to component: <AdvancedImage cldImg={img} />

Video Transformations

  • ✅ Chain transformations on video instance (same pattern as images):
    const video = cld.video('id')
      .resize(fill().width(800).height(600))
      .delivery(format(auto()));
    
  • ✅ Pass to component: <AdvancedVideo cldVid={video} />
  • ✅ Video transformations work the same way as image transformations

Transformation Best Practices

  • ✅ Format and quality must use separate .delivery() calls
  • ✅ Always end with auto format/quality: .delivery(format(auto())).delivery(quality(autoQuality())) unless user specifies a particular format or quality
  • ✅ Use gravity(auto()) unless user specifies a focal point
  • ✅ Same transformation syntax works for both images and videos

Plugin Patterns

  • When the user asks for lazy loading or responsive images: Use the Cloudinary plugins from @cloudinary/reactresponsive(), lazyload(), placeholder() — with AdvancedImage. Do not use only native loading="lazy" or CSS-only responsive; the Cloudinary plugins handle breakpoints, lazy loading, and placeholders for Cloudinary URLs.
  • ✅ Import plugins from @cloudinary/react
  • ✅ Pass plugins as array: plugins={[responsive(), lazyload(), placeholder()]}
  • ✅ Recommended plugin order:
    1. responsive() - First (handles breakpoints)
    2. placeholder() - Second (shows placeholder while loading)
    3. lazyload() - Third (delays loading until in viewport)
    4. accessibility() - Last (if needed)
  • ✅ Always add width and height attributes to prevent layout shift
  • ✅ Example:
    <AdvancedImage
      cldImg={img}
      plugins={[responsive(), placeholder({ mode: 'blur' }), lazyload()]}
      width={800}
      height={600}
    />
    

Responsive Images Pattern

  • Responsive images: Use the Cloudinary responsive() plugin with fill() resize (not only CSS). Lazy loading: Use the Cloudinary lazyload() plugin with AdvancedImage (not only loading="lazy").
  • ✅ Use responsive() plugin with fill() resize
  • ✅ Combine with placeholder() and lazyload() plugins
  • ✅ Example:
    const img = cld.image('id').resize(fill().width(800));
    <AdvancedImage 
      cldImg={img} 
      plugins={[responsive(), placeholder({ mode: 'blur' }), lazyload()]} 
      width={800}
      height={600}
    />
    

Image gallery with lazy loading and responsive

  • When the user asks for an image gallery with lazy loading and responsive: Use Cloudinary plugins with AdvancedImage: responsive(), lazyload(), placeholder() (see Plugin Patterns). Use fill() resize with the responsive plugin. Add width and height to prevent layout shift.
  • Sample assets in galleries: Use the sample public IDs from "Creating Image & Video Instances" (e.g. samples/bike, samples/landscapes/beach-boat, samples/food/spices, samples/two-ladies, samples/landscapes/girl-urban-view, samples/animals/reindeer, samples/food/pot-mussels, samples/cloudinary-icon). Assume any sample might not exist — users can delete them. Start with one reliable sample (e.g. samples/cloudinary-icon) or a short list; add onError handling and remove/hide failed images. Prefer uploaded assets when available (e.g. from UploadWidget) over samples.
  • Handle load errors: Use onError on AdvancedImage to hide or remove failed images (e.g. set state to filter out the publicId, or hide the parent). Provide user feedback (e.g. "Some images could not be loaded. Try uploading your own!") and upload functionality so users can add their own images.
  • Fallback: Default gallery list can be a subset of the sample list (e.g. ['samples/cloudinary-icon', 'samples/bike', 'samples/landscapes/beach-boat']); when user uploads, append result.public_id. If an image fails to load, remove it from the list or hide it so the UI doesn't show broken images.

Image Overlays (text or logos)

  • When the user asks for image overlays with text or logos: Use @cloudinary/url-gen overlay APIs. Copy imports and patterns from the "Import reference" table and "Canonical overlay block" in these rules. Do not import text or image from actions/overlay — they are from qualifiers/source; only source is from actions/overlay.
  • Import source from actions/overlay; text and image from qualifiers/source. Also: Position from qualifiers/position, TextStyle from qualifiers/textStyle, compass from qualifiers/gravity, Transformation from transformation/Transformation, scale from actions/resize.
  • compass() takes string values, with underscores: compass('center'), compass('south_east'), compass('north_west'). ❌ WRONG: compass(southEast) or 'southEast' (no camelCase).
  • Overlay image: Use new Transformation() inside .transformation(): image('logo').transformation(new Transformation().resize(scale().width(100))). ❌ WRONG: image('logo').transformation().resize(...) (.transformation() does not return a chainable object).
  • Text overlay: fontWeight goes on TextStyle: new TextStyle('Arial', 60).fontWeight('bold'). textColor goes on the text source (chained after text(...)): text('Hello', new TextStyle('Arial', 60)).textColor('white').
  • Position is chained after source(...), not inside: source(image('logo').transformation(...)).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20)).
  • Image overlay pattern: baseImage.overlay(source(image('id').transformation(new Transformation().resize(scale().width(100)))).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))). (Import scale from @cloudinary/url-gen/actions/resize if needed.)
  • Text overlay pattern: baseImage.overlay(source(text('Your Text', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white')).position(new Position().gravity(compass('center')))).
  • ✅ Docs: React Image Transformations and transformation reference for overlay syntax.

Upload Widget Pattern

  • ✅ Use component: import { UploadWidget } from './cloudinary/UploadWidget'

Strict initialization pattern (always follow this exactly):

  1. Script in index.html (required):
<script src="https://upload-widget.cloudinary.com/global/all.js" async></script>
  1. Poll in useEffect until createUploadWidget is available (required): Use setInterval (e.g. every 100ms) to check typeof window.cloudinary?.createUploadWidget === 'function'. Only create the widget when this returns true. Clear the interval once ready.
  2. Add a timeout (e.g. 10 seconds) to stop polling and show an error state if the script never loads. Clear both interval and timeout in cleanup and when ready.
  3. Create widget once, store in a ref. Cleanup: clear interval, clear timeout, remove click listener.

Do NOT: Check only window.cloudinary (the function may not be attached yet); do a single check in onload (unreliable timing); skip index.html and rely only on dynamic injection; poll forever without a timeout.

  • ✅ Create unsigned upload preset in dashboard at settings/upload/presets
  • ✅ Add to .env: VITE_CLOUDINARY_UPLOAD_PRESET=your_preset_name
  • ✅ Handle callbacks:
    <UploadWidget
      onUploadSuccess={(result) => {
        console.log('Public ID:', result.public_id);
      }}
      onUploadError={(error) => {
        console.error('Upload failed:', error);
      }}
    />
    
  • ✅ Upload result contains: public_id, secure_url, width, height, etc.

For complete Upload Widget configuration options (all properties for createUploadWidget), see the official reference: https://cloudinary.com/documentation/upload_widget_reference.md

Signed vs unsigned uploads (when to use which)

Unsigned uploads (simpler, no backend required):

  • Use when: Quick prototypes, low-risk apps, or when anyone with the preset name may upload.
  • Preset: Create an Unsigned upload preset in Cloudinary dashboard (Settings → Upload → Upload presets). Put preset name in .env as VITE_CLOUDINARY_UPLOAD_PRESET.
  • Client: Widget needs only cloudName and uploadPreset. No API key or secret; no backend.
  • Trade-off: Anyone who knows the preset name can upload. Use only when that is acceptable.

Signed uploads (more secure, backend required):

  • Use when: Production apps, authenticated users, or when you need to control who can upload.
  • Preset: Create a Signed upload preset in the dashboard. The backend generates a signature using your API secret; the client never sees the secret.
  • Client: Widget gets api_key (from your backend), uploadPreset, and an uploadSignature function that calls your backend for each upload. API secret stays on server only.
  • Trade-off: Requires a backend (Node/Express, Next.js API route, etc.) to sign requests. More secure; signature validates each upload.

Rule of thumb: Default to unsigned uploads unless the user explicitly asks for "secure" or "signed" uploads. Do not default to signed — it requires a running backend and will fail out of the box. Use signed only when the user explicitly requests secure/signed uploads or needs to restrict who can upload.

Secure (Signed) Uploads

When to use: Production apps, authenticated users, or when you need to control who can upload (more secure than unsigned).

Golden rules:

  1. Never expose or commit the API secret (server-only)
  2. Use server/.env in .gitignore for API key/secret
  3. API key can be sent to client; API secret must stay server-only

Quick implementation:

  • Use uploadSignature as function (not signatureEndpoint)
  • Fetch api_key from server before creating widget
  • Include uploadPreset in widget config
  • Server includes upload_preset in signed params
  • Use Cloudinary Node.js SDK v2 on server

For complete implementation patterns, client/server code examples, and troubleshooting, see references/signed-uploads.md

Video Patterns

  • Display a video → Use AdvancedVideo (@cloudinary/react). It just displays a video (with optional transformations). Not a full player.
  • A video player → Use Cloudinary Video Player (cloudinary-video-player). That is the actual player (styled UI, controls, playlists, etc.).

⚠️ IMPORTANT: Two Different Approaches

1. AdvancedVideo (@cloudinary/react) — For displaying a video

  • React component similar to AdvancedImage; just displays a video with Cloudinary transformations
  • Not a full "player" — it's video display (native HTML5 video with optional controls)
  • Use when: user wants to show/display a video. Works with cld.video() like images with cld.image()

2. Cloudinary Video Player (cloudinary-video-player) — The player

  • Full-featured video player (styled UI, controls, playlists). Use when the user asks for a "video player."
  • Use imperative video element only (create with document.createElement, append to container ref); do not pass a React-managed <video ref>. See "Cloudinary Video Player (The Player)" below.

AdvancedVideo (React SDK - For Displaying a Video)

  • Purpose: Display a video with Cloudinary transformations (resize, effects, etc.). It is not a full player — it is for showing a video. For a player, use Cloudinary Video Player.
  • Package: @cloudinary/react (same as AdvancedImage)
  • Import: import { AdvancedVideo } from '@cloudinary/react'
  • NO CSS IMPORT NEEDED: AdvancedVideo uses native HTML5 video - no CSS import required
  • WRONG: import '@cloudinary/react/dist/cld-video-player.css' (this path doesn't exist)
  • Create video instance: const video = cld.video(publicId) (like cld.image())
  • Apply transformations: Chain transformations like images:
    const video = cld.video('video-id')
      .resize(fill().width(800).height(600))
      .delivery(format(auto()));
    
  • Use component:
    <AdvancedVideo
      cldVid={video}
      controls
      autoplay
      muted
    />
    
  • Documentation: https://cloudinary.com/documentation/react_video_transformations.md

Cloudinary Video Player (The Player)

Use when the user asks for a video player (styled UI, controls, playlists). For just displaying a video, use AdvancedVideo instead.

Critical rule: Imperative element only

  • ❌ Do NOT pass React-managed <video ref> (causes removeChild errors)
  • ✅ Use document.createElement('video'), append to container ref, pass to videoPlayer(el, ...)

Quick setup:

  • Package: npm install cloudinary-video-player
  • Import: import { videoPlayer } from 'cloudinary-video-player' and CSS
  • Source: player.source({ publicId }) (object, not string)
  • Cleanup: player.dispose() then if (el.parentNode) el.parentNode.removeChild(el)
  • Always include posterOptions: { transformation: { startOffset: '0' }, posterColor: '#0f0f0f' }

For complete implementation pattern, cleanup, error handling, and troubleshooting, see references/video-player.md

When to Use Which?

  • Use AdvancedVideo when: User wants to display or show a video (no full player). It just displays a video with transformations.
  • Use Cloudinary Video Player when: User asks for a video player — the actual player with styled UI, controls, and optional features (playlists, ads, etc.).

TypeScript Patterns

Essential TypeScript usage:

  • Type imports: import type { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen'
  • Upload results: Define CloudinaryUploadResult interface
  • Environment variables: Create vite-env.d.ts with ImportMetaEnv interface
  • Avoid any: Use proper interfaces or unknown with type guards
  • Type refs: useRef<HTMLVideoElement>(null), useRef<unknown>(null) for widgets

For complete TypeScript patterns, type guards, ref typing, and error solutions, see references/typescript-patterns.md

Best Practices

  • ✅ Always use fill() resize with automatic gravity for responsive images
  • ✅ Always end transformations with .delivery(format(auto())).delivery(quality(autoQuality())) unless the user specifies a format or quality
  • ✅ Use placeholder() and lazyload() plugins together
  • ✅ Always add width and height attributes to AdvancedImage
  • ✅ Store public_id from upload success, not full URL
  • ✅ Video player: use imperative element only; dispose in useLayoutEffect cleanup and remove element with if (el.parentNode) el.parentNode.removeChild(el); always include posterOptions with transformation: { startOffset: '0' } and posterColor: '#0f0f0f' for reliable poster display
  • ✅ Use TypeScript for better autocomplete and error catching
  • ✅ Prefer unknown over any when types aren't available
  • ✅ Use type guards for runtime type checking
  • ✅ Define interfaces for Cloudinary API responses
  • ✅ Create vite-env.d.ts for environment variable typing
  • ✅ Use proper HTML element types for refs

⚠️ COMMON ERRORS & SOLUTIONS

For detailed error solutions and troubleshooting, see references/troubleshooting.md

Quick Error Reference

Environment Variable Errors

  • Env vars: Wrong prefix, missing VITE_, not restarted → Use correct bundler prefix, restart, clear cache
  • Imports: Wrong package/path → Use exact paths from Import reference table
  • Upload Widget: "createUploadWidget is not a function" → Poll with setInterval, don't check only window.cloudinary
  • Transformations: Not working → Chain properly, use v2 syntax, separate format/quality
  • Video Player: removeChild errors → Use imperative element (createElement), not React ref
  • TypeScript: Type errors → See references/typescript-patterns.md
  • Signed uploads: Invalid signature → See references/signed-uploads.md

Most Common Issues

  1. Environment variables undefined: Clear node_modules/.vite/, restart dev server, hard refresh browser
  2. "createUploadWidget is not a function": Poll with setInterval until typeof window.cloudinary?.createUploadWidget === 'function'
  3. Wrong imports: Use ONLY paths from Import reference table - don't guess subpaths
  4. Upload fails: Check preset exists, is Unsigned (for unsigned), dev server restarted
  5. Video player errors: Use imperative element only, include posterOptions
  6. Overlay issues: Import text/image from qualifiers/source, not actions/overlay

For complete troubleshooting with detailed solutions, see references/troubleshooting.md

Related skills
Installs
77
GitHub Stars
4
First Seen
Apr 7, 2026