> ## Documentation Index
> Fetch the complete documentation index at: https://cloudinary.com/documentation/llms.txt
> Use this file to discover all available pages before exploring further.

# Interactive content and controls



User interface components and navigation must be operable by all users, regardless of their physical abilities or the input methods they use. This means ensuring that users can interact with and navigate your content using various methods including keyboards, screen readers, voice commands, or other assistive technologies.

For media content, operability encompasses several key areas: providing alternatives to motion-based content for users with vestibular disorders, ensuring all interactive elements are keyboard accessible, and designing interfaces that work seamlessly with assistive technologies. Users with motor impairments, visual disabilities, or other conditions need content that responds predictably to their preferred interaction methods.

This section covers Cloudinary's tools and widgets that support operable interfaces, including techniques for managing animated content, implementing keyboard-accessible galleries, and creating video players that work with assistive technologies.

## Operability considerations
Consider how users will interact with and control your media content. Some users rely on keyboards instead of mice, others may be sensitive to motion and flashing content, and many need the ability to pause or adjust audio that plays automatically. Design your media experiences to accommodate different interaction methods and user preferences.

{table:class=no-borders overview accessibility-considerations} Consideration | Cloudinary Image Techniques | WCAG Reference
---|---|---
**Consider users who may experience seizures from flashing content or be sensitive to motion and animations.** Avoid content that flashes more than three times per second, and provide options to disable or reduce motion effects that aren't essential to understanding your content. | 🔧 [Convert animations to still images](accessible_media_interactive_controls#convert_animations_to_still_images) | [2.3.1](https://www.w3.org/TR/WCAG22/#three-flashes-or-below-threshold) Three flashes or below threshold[2.3.2](https://www.w3.org/TR/WCAG22/#three-flashes) Three flashes[2.3.3](https://www.w3.org/TR/WCAG22/#animation-from-interactions) Animation from interactions

Cloudinary's UI widgets have been built with operability considerations in mind, including keyboard navigation, focus management, and user control features. For detailed information about the accessibility features available in these components, see the [Cloudinary Product Gallery widget](accessible_media_interactive_controls#cloudinary_product_gallery_widget), [Cloudinary Video Player](accessible_media_interactive_controls#cloudinary_video_player), and [Cloudinary Upload widget](accessible_media_interactive_controls#cloudinary_upload_widget) sections. 

## Convert animations to still images

Many users have vestibular disorders, seizure conditions, or other sensitivities that make animated content problematic or even dangerous. Additionally, some users simply prefer reduced motion for better focus and less distraction.

Cloudinary provides several approaches to make animated content accessible by converting animations to still images or providing user control over motion.

### Understanding motion sensitivity

Users may need reduced motion for various reasons:

* **Vestibular disorders**: Inner ear conditions that cause dizziness and nausea from motion
* **Seizure disorders**: Flashing or rapid motion can trigger seizures
* **Attention disorders**: Animation can be distracting and make it difficult to focus
* **Migraine triggers**: Motion can trigger or worsen migraines
* **Battery conservation**: Reducing animation saves device battery life
* **Bandwidth limitations**: Still images use less data than animated content

### Extracting still frames from animations

You can extract a single frame from an animated GIF or video to create a still image alternative. This is useful for providing a static version of animated content.

Original animation

Still frame (page 3)

Use the [page parameter](transformation_reference#pg_page_or_file_layer) (`pg_`) to extract a specific frame from an animated GIF:

![Still frame extracted from animated GIF](https://res.cloudinary.com/demo/image/upload/pg_3/kitten_fighting.gif "with_image:false")

```nodejs
cloudinary.image("kitten_fighting.gif", {page: 3})
```

```react
new CloudinaryImage("kitten_fighting.gif").extract(getPage().byNumber(3));
```

```vue
new CloudinaryImage("kitten_fighting.gif").extract(getPage().byNumber(3));
```

```angular
new CloudinaryImage("kitten_fighting.gif").extract(getPage().byNumber(3));
```

```js
new CloudinaryImage("kitten_fighting.gif").extract(getPage().byNumber(3));
```

```python
CloudinaryImage("kitten_fighting.gif").image(page=3)
```

```php
(new ImageTag('kitten_fighting.gif'))
	->extract(Extract::getPage()->byNumber(3));
```

```java
cloudinary.url().transformation(new Transformation().page(3)).imageTag("kitten_fighting.gif");
```

```ruby
cl_image_tag("kitten_fighting.gif", page: 3)
```

```csharp
cloudinary.Api.UrlImgUp.Transform(new Transformation().Page(3)).BuildImageTag("kitten_fighting.gif")
```

```dart
cloudinary.image('kitten_fighting.gif').transformation(Transformation()
	.extract(Extract.getPage().byNumber(3)));
```

```swift
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setPage(3)).generate("kitten_fighting.gif")!, cloudinary: cloudinary)
```

```android
MediaManager.get().url().transformation(new Transformation().page(3)).generate("kitten_fighting.gif");
```

```flutter
cloudinary.image('kitten_fighting.gif').transformation(Transformation()
	.extract(Extract.getPage().byNumber(3)));
```

```kotlin
cloudinary.image {
	publicId("kitten_fighting.gif")
	 extract(Extract.getPage() { byNumber(3) }) 
}.generate()
```

```jquery
$.cloudinary.image("kitten_fighting.gif", {page: 3})
```

```react_native
new CloudinaryImage("kitten_fighting.gif").extract(getPage().byNumber(3));
```

You can also extract the first frame by converting the format to a static image format like JPG or PNG:

![First frame as JPG](https://res.cloudinary.com/demo/image/upload/f_jpg/kitten_fighting.gif "with_image:false")

```nodejs
cloudinary.image("kitten_fighting.gif", {fetch_format: "jpg"})
```

```react
new CloudinaryImage("kitten_fighting.gif").delivery(format(jpg()));
```

```vue
new CloudinaryImage("kitten_fighting.gif").delivery(format(jpg()));
```

```angular
new CloudinaryImage("kitten_fighting.gif").delivery(format(jpg()));
```

```js
new CloudinaryImage("kitten_fighting.gif").delivery(format(jpg()));
```

```python
CloudinaryImage("kitten_fighting.gif").image(fetch_format="jpg")
```

```php
(new ImageTag('kitten_fighting.gif'))
	->delivery(Delivery::format(
	Format::jpg()));
```

```java
cloudinary.url().transformation(new Transformation().fetchFormat("jpg")).imageTag("kitten_fighting.gif");
```

```ruby
cl_image_tag("kitten_fighting.gif", fetch_format: "jpg")
```

```csharp
cloudinary.Api.UrlImgUp.Transform(new Transformation().FetchFormat("jpg")).BuildImageTag("kitten_fighting.gif")
```

```dart
cloudinary.image('kitten_fighting.gif').transformation(Transformation()
	.delivery(Delivery.format(
	Format.jpg())));
```

```swift
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setFetchFormat("jpg")).generate("kitten_fighting.gif")!, cloudinary: cloudinary)
```

```android
MediaManager.get().url().transformation(new Transformation().fetchFormat("jpg")).generate("kitten_fighting.gif");
```

```flutter
cloudinary.image('kitten_fighting.gif').transformation(Transformation()
	.delivery(Delivery.format(
	Format.jpg())));
```

```kotlin
cloudinary.image {
	publicId("kitten_fighting.gif")
	 delivery(Delivery.format(
	Format.jpg())) 
}.generate()
```

```jquery
$.cloudinary.image("kitten_fighting.gif", {fetch_format: "jpg"})
```

```react_native
new CloudinaryImage("kitten_fighting.gif").delivery(format(jpg()));
```

### Implementing user-controlled motion preferences

The most accessible approach is to respect user preferences for reduced motion. Modern browsers support the [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) CSS media query, which you can combine with Cloudinary transformations to serve appropriate content.

Here's an interactive demo showing how to implement motion preferences:

  🎬 Enable Motion
  🚫 Reduce Motion

  
  
    Current setting: Motion enabled
  

  Current image URL:
  https://res.cloudinary.com/demo/image/upload/c_scale,w_400/kitten_fighting.gif

### Implementation examples

Here are some examples of respecting the `prefers-reduced-motion` setting:

React:

```react
import React, { useState, useEffect } from 'react';

const AccessibleAnimatedImage = ({ publicId, alt, ...props }) => {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
  
  useEffect(() => {
    // Check user's motion preference
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReducedMotion(mediaQuery.matches);
    
    // Listen for changes
    const handleChange = (e) => setPrefersReducedMotion(e.matches);
    mediaQuery.addEventListener('change', handleChange);
    
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);
  
  const imageUrl = prefersReducedMotion
    ? `https://res.cloudinary.com/demo/image/upload/pg_3/f_auto/q_auto/${publicId}.gif`
    : `https://res.cloudinary.com/demo/image/upload/${publicId}.gif`;
  
  return (
    <img 
      src={imageUrl} 
      alt={alt}
      {...props}
    />
  );
};

export default AccessibleAnimatedImage;
```

HTML:

```html
<!-- Responsive image that respects motion preferences -->
<picture>
  <!-- Show still image for users who prefer reduced motion -->
  <source 
    media="(prefers-reduced-motion: reduce)" 
    srcset="https://res.cloudinary.com/demo/image/upload/c_scale,w_800/pg_3/ff_auto/q_auto/kitten_fighting.gif 800w,
            https://res.cloudinary.com/demo/image/upload/c_scale,w_400/pg_3/f_auto/q_autog/kitten_fighting.gif 400w"
    sizes="(max-width: 600px) 400px, 800px">
  
  <!-- Show animated content for users who don't mind motion -->
  <source 
    media="(prefers-reduced-motion: no-preference)" 
    srcset="https://res.cloudinary.com/demo/image/upload/c_scale,w_800/kitten_fighting.gif 800w,
            https://res.cloudinary.com/demo/image/upload/c_scale,w_400/kitten_fighting.gif 400w"
    sizes="(max-width: 600px) 400px, 800px">
  
  <!-- Fallback image for browsers that don't support the media queries -->
  <img 
    src="https://res.cloudinary.com/demo/image/upload/c_scale,w_800/pg_3/f_auto/q_auto/kitten_fighting.gif" 
    alt="Playful kittens interacting"
    loading="lazy">
</picture>
```

CSS:

```css

/* Default: show animated version */
.motion-content {
  background-image: url('https://res.cloudinary.com/demo/image/upload/kitten_fighting.gif');
}

/* Reduced motion: show still frame */
@media (prefers-reduced-motion: reduce) {
  .motion-content {
    background-image: url('https://res.cloudinary.com/demo/image/upload/pg_3/f_auto/q_auto/kitten_fighting.gif');
  }
}

```

### Video posters

For video content, you can extract a poster frame to show when motion is reduced:

```html
<!-- Animated video with still poster -->
<video 
  poster="https://res.cloudinary.com/demo/video/upload/so_10.2/docs/grocery-store.jpg"
  controls
  preload="none"
>
  <source src="https://res.cloudinary.com/demo/video/upload/docs/grocery-store.mp4" type="video/mp4">
</video>
```

![Animated video with still poster](https://res.cloudinary.com/demo/video/upload/docs/grocery-store.mp4 "thumb:c_scale,w_300, poster:https://res.cloudinary.com/demo/video/upload/so_10.2/c_scale,w_300/docs/grocery-store.jpg, with_code:false, with_url:false")

The poster frame uses the [start offset](transformation_reference#so_start_offset) (`so_`) parameter to extract a frame from 10.2 seconds into the video.

You can also use `so_auto` to let Cloudinary automatically choose the best frame to use as the poster.

> **TIP**:
>
> :title=Best practices for motion accessibility

> * **Respect system preferences**: Always check for `prefers-reduced-motion: reduce`

> * **Provide user controls**: Allow users to toggle motion on/off regardless of system settings

> * **Choose meaningful still frames**: Select frames that best represent the animated content

> * **Maintain functionality**: Ensure that stopping animation doesn't break essential features

> * **Test with users**: Verify that reduced motion versions are still informative and useful

> * **Consider alternatives**: Sometimes a different approach (like a [slideshow](cloudinary_cli#examples-1)) works better than a single still frame

> **READING**:
>
> * Docs: [Deliver a single frame of an animated image](animated_images#deliver_a_single_frame)

> * Docs: [Video thumbnails](video_effects_and_enhancements#video_thumbnails)

> * Docs: [Page parameter for animated images](transformation_reference#pg_page_or_file_layer)

> * Video tutorial: [Reduce motion of images in React](https://www.youtube.com/watch?v=SjPWxxk0NXM)

## Cloudinary Product Gallery widget

The [Cloudinary Product Gallery widget](product_gallery) provides comprehensive accessibility features that ensure users with disabilities can effectively navigate and interact with product galleries. The widget includes keyboard navigation, screen reader support, and customizable display options that meet WCAG operability requirements.

  🎯 Accessible Product Gallery Demo
  
    Keyboard Navigation: Use Tab to navigate, Enter to view items, Escape to close zoom, Arrow keys to browse between media assets.
  
  

### Keyboard accessibility

The Product Gallery enables full keyboard accessibility for users who can't use a mouse or rely on assistive technologies. All interactive elements are accessible using standard keyboard navigation:

  Keyboard Navigation Controls
  
    Key
    Action
    
    Tab
    Navigate forward through interactive elements
    
    Shift + Tab
    Navigate backward through interactive elements
    
    Enter
    View an asset or activate zoom
    
    Escape
    Exit zoom mode or close overlays
    
    Spacebar
    Play/pause videos
    
    Arrow Keys
    Navigate between gallery items
  

### Accessible configuration options

For the most accessible experience, Cloudinary recommends these configuration settings:

```javascript
const accessibleGallery = cloudinary.galleryWidget({
  container: "#my-gallery",
  cloudName: "demo",
  mediaAssets: [{ tag: "product_demo" }],
  
  // Expanded mode provides more prominent focus indicators
  displayProps: { 
    mode: "expanded" 
  },
  
  // Popup zoom is more accessible than inline zoom
  zoomProps: { 
    type: "popup" 
  },
  
  // Simplified video controls reduce complexity
  videoProps: { 
    controls: "play" 
  },
  
  // Configure accessible text alternatives
  accessibilityProps: {
    mediaAltSource: "metadata",
    mediaAltId: "alt_text"
  }
});

accessibleGallery.render();
```

### Screen reader support

The Product Gallery provides semantic markup for screen readers and uses alt text from your [configured metadata sources](accessible_media_images#managing_text_alternatives). You can specify where the gallery should look for alt text using the `accessibilityProps` parameter:

**Using structured metadata for alt text:**

```javascript
const galleryWithAltText = cloudinary.galleryWidget({
  container: "#my-gallery",
  cloudName: "demo",
  mediaAssets: [{ tag: "shoes" }],
  accessibilityProps: {
    mediaAltSource: "metadata",
    mediaAltId: "product_description"
  }
});
```

**Using contextual metadata for alt text:**

```javascript
const galleryWithContextualAlt = cloudinary.galleryWidget({
  container: "#my-gallery", 
  cloudName: "demo",
  mediaAssets: [{ tag: "bags" }],
  accessibilityProps: {
    mediaAltSource: "contextual",
    mediaAltId: "alt_description"
  }
});
```

If no alt text source is configured or the specified metadata field is empty, the gallery defaults to descriptive text in the format "Gallery asset n of m".

### Focus management and visual indicators

The Product Gallery provides clear visual focus indicators that help users understand their current position within the gallery:

* **High contrast focus rings**: Clearly visible borders around focused elements
* **Logical tab order**: Sequential navigation through thumbnails, main viewer, and controls
* **Focus trapping**: When zoom is activated, focus remains within the zoom interface
* **Expanded mode benefits**: In expanded mode, focus areas are more visually prominent

### Video accessibility features

When displaying videos in the Product Gallery, accessibility features include:

* **Keyboard controls**: Spacebar to play/pause, Enter to activate full controls
* **Screen reader announcements**: Video state changes are announced to assistive technology
* **Simplified controls**: The `controls: "play"` option reduces interface complexity
* **Caption support**: When using the Cloudinary Video Player, captions and subtitles are fully supported

```javascript
const accessibleVideoGallery = cloudinary.galleryWidget({
  container: "#my-gallery",
  cloudName: "demo", 
  mediaAssets: [
    { tag: "product_videos", mediaType: "video" }
  ],
  videoProps: {
    playerType: "cloudinary",
    controls: "play",
    // Additional Video Player accessibility options
    textTracks: {
      captions: {
        label: "English (Captions)",
        default: true,
        url: "https://res.cloudinary.com/demo/raw/upload/product_captions.vtt"
      }
    }
  }
});
```

### Responsive accessibility

The Product Gallery maintains accessibility across different viewport sizes:

* **Mobile optimization**: Touch-friendly controls and appropriate sizing
* **Viewport breakpoints**: Maintains usability as layout adapts to screen size
* **Consistent navigation**: Keyboard accessibility preserved across all breakpoints

```javascript
const responsiveAccessibleGallery = cloudinary.galleryWidget({
  container: "#my-gallery",
  cloudName: "demo",
  mediaAssets: [{ tag: "products" }],
  viewportBreakpoints: [
    { 
      breakpoint: 600, 
      carouselStyle: "thumbnails", 
      carouselLocation: "bottom" 
    },
    { 
      breakpoint: 300, 
      carouselStyle: "indicators", 
      carouselLocation: "bottom",
      navigation: "always"  // Always show navigation for small screens
    }
  ]
});
```

> **TIP**:
>
> :title=Best practices for accessible Product Galleries

> * **Provide meaningful alt text**: Use structured or contextual metadata to supply descriptive alt text for all images

> * **Use expanded mode for better focus visibility**: The expanded display mode provides more prominent focus indicators

> * **Simplify video controls**: Use `controls: "play"` to reduce cognitive load

> * **Test with keyboard only**: Ensure all functionality is accessible without a mouse

> * **Provide captions for videos**: When using videos, include captions or transcripts

> * **Consider loading states**: Ensure loading indicators are announced to screen readers

> * **Test with screen readers**: Verify that the gallery provides a logical and informative experience for screen reader users

> **READING**:
>
> * Docs: [Product Gallery accessibility](product_gallery#accessibility)

> * Video tutorial: [Product Gallery accessibility features](product_gallery_accessibility_tutorial)

> * Code example: [Accessible Product Gallery sandbox](https://codesandbox.io/p/devbox/jolly-mayer-lssy4c)

## Cloudinary Video Player

The [Cloudinary Video Player](cloudinary_video_player) is designed to provide an inclusive video experience that meets WCAG 2.1 AA compliance standards. The player includes comprehensive accessibility features that ensure users with visual, auditory, motor, and cognitive impairments can fully engage with video content through assistive technologies, keyboard navigation, and other accessibility-friendly enhancements.

### Accessibility features overview

The Cloudinary Video Player provides extensive accessibility support including:

* **Full keyboard navigation**: All controls accessible via Tab key with clear focus indicators
* **Screen reader compatibility**: ARIA attributes and semantic markup for assistive technologies
* **Closed captions and subtitles**: Multi-language support with customizable styling (refer to [Video captions](accessible_media_video_audio#video_captions))
* **Audio descriptions**: Support for descriptive audio tracks and caption-based descriptions (refer to [Audio descriptions](accessible_media_video_audio#audio_descriptions))
* **Video chapters**: Easy navigation to key sections for improved usability
* **Adjustable playback**: Variable speed controls for better comprehension
* **High-contrast UI**: Customizable themes for improved visibility (refer to [Customizable caption styling](accessible_media_visual_audio_clarity#customizable_caption_styling))

> **TIP**: For full details, see the [Video Player accessibility](video_player_accessibility) guide.

### Live accessibility demo

Here's a working Cloudinary Video Player demonstrating accessibility features. Try navigating the controls using only your keyboard (Tab, Space, Arrow keys) and notice the clear focus indicators:

  🎬 Accessible Video Player Demo
  
    Keyboard Controls: Tab to navigate controls, Space to play/pause, Arrow keys to seek. Use Tab to reach mute, fullscreen, and caption buttons.
  
  
  
  
  
    Accessibility Features Demonstrated:
    
      Keyboard navigation with visible focus indicators
      Closed captions with high-contrast styling
      Screen reader compatible controls
      Customizable playback speed
      ARIA labels and semantic markup
    
  

### Keyboard navigation controls

  Video Player Keyboard Controls
  
    Key
    Action
    
    Tab
    Navigate forward through video player controls
    
    Shift + Tab
    Navigate backward through video player controls
    
    Spacebar
    Play/pause video
    
    Enter
    Activate focused button (play, fullscreen, etc.)
    
    Left Arrow
    Seek backward 10 seconds
    
    Right Arrow
    Seek forward 10 seconds
    
    Up Arrow
    Increase volume
    
    Down Arrow
    Decrease volume
        
    Escape
    Exit fullscreen mode
  

### Implementation example

Here's how to configure the Cloudinary Video Player with optimal accessibility settings:

```javascript
const accessiblePlayer = cloudinary.videoPlayer('my-player', {
  cloudName: 'your-cloud-name',
  
  // High contrast theme for better visibility
  colors: {
    base: '#000000',      // Dark background for controls
    text: '#ffffff',      // White text for high contrast
    accent: '#0066cc'     // Blue accent color
  },
  
  // Enable all accessibility features
  controls: true,
  fluid: true,
  focusable: true
});

// Load video with accessibility features
accessiblePlayer.source('your-video', {
  textTracks: { 
    captions: { 
      label: 'English (Captions)',
      default: true,
      url: 'path/to/captions.vtt'
    },
    chapters: {
      label: 'Chapters',
      default: true,
      url: 'path/to/chapters.vtt'
    },
    options: {
      // WCAG-compliant caption styling
      theme: 'videojs-default',
      fontSize: '16px',
      fontFace: 'Arial, sans-serif'
    }
  }
});
```

> **READING**:
>
> * Docs: [Video Player accessibility](video_player_accessibility)

> * Docs: [Cloudinary Video Player](cloudinary_video_player)

> * Docs: [Video Player customization](video_player_customization)

> * Docs: [Subtitles and captions](video_player_customization#subtitles_and_captions)

> * Video tutorials: [Video Player tutorials](video_player_tutorials)

## Cloudinary Upload widget

The [Cloudinary Upload widget](upload_widget) provides a complete, interactive user interface that enables your users to upload files from a variety of sources. When configured with an accessible color theme, the Upload widget is designed to provide an inclusive asset sharing experience that meets **WCAG 2.1 AA** compliance standards.

### Accessibility features overview

The Upload widget includes comprehensive accessibility support to ensure users with disabilities can effectively upload and manage their media:

* **Full keyboard navigation**: Tab and Shift+Tab to navigate through all interface elements with clear focus indicators
* **Screen reader compatibility**: Semantic markup, accessible labels, and ARIA attributes for assistive technologies
* **High-contrast accessible theme**: Predefined color palette meeting WCAG contrast requirements
* **Zoom and text scaling**: Interface adapts to user-defined text size preferences
* **Focus management**: Logical tab order with focus trapping in modal panels
* **Live region announcements**: Status updates announced to screen readers

### Motor and mobility support

For users who rely on keyboard navigation instead of a mouse, the Upload widget provides:

* Sequential navigation through all interface elements using Tab and Shift+Tab
* Logical and predictable focus order through source tabs, file selection, and controls
* Focus trapping in panels to prevent loss of context when dialog boxes open
* All actions accessible via keyboard (Enter to activate, Escape to close)

#### Keyboard navigation controls

  Upload Widget Keyboard Controls
  
    Key
    Action
    
    Tab
    Navigate forward through sources, buttons, and controls
    
    Shift + Tab
    Navigate backward through interface elements
    
    Enter
    Activate buttons and select source tabs
    
    Escape
    Close the widget or cancel current operation
    
    Spacebar
    Toggle checkboxes and activate buttons
  

### Screen reader support

The Upload widget is designed to work seamlessly with screen readers through:

* Programmatically associated labels for all form controls
* Clear, descriptive names for actionable elements like buttons and links
* Alternative text for informative images
* Semantic HTML structure with proper heading hierarchy
* Unique landmark labels for efficient navigation
* Live regions that announce filter results and status changes

### Accessible theme configuration

The Upload widget includes a predefined accessible theme with sufficient color contrast ratios. You can apply this theme using the `styles` parameter:

  📤 Accessible Upload Widget Demo
  
    Keyboard Navigation: Tab to navigate to the button, Enter to open, Tab through sources and controls. This widget uses the accessible theme with high-contrast colors.
  
  
  
    
      Upload Files (Accessible Theme)
    
  
  
  
    Accessibility Features:
    
      High-contrast accessible color theme
      Full keyboard navigation with visible focus indicators
      Screen reader compatible labels and announcements
      Logical focus order and focus trapping
      WCAG 2.1 AA compliant
    
  

### Implementation example

Here's how to configure the Upload widget with the accessible theme:

```javascript
const accessibleUploadWidget = cloudinary.createUploadWidget({
  cloudName: 'your-cloud-name',
  uploadPreset: 'your-preset',
  
  // Accessible color theme with WCAG 2.1 AA compliant contrast ratios
  styles: {
    palette: {
      window: "#FFFFFF",
      windowBorder: "#6A7481",
      tabIcon: "#3448C5",
      menuIcons: "#5A616A",
      textDark: "#000000",
      textLight: "#FFFFFF",
      link: "#3448C5",
      action: "#3448C5",
      inactiveTabIcon: "#0E2F5A",
      error: "#F44235",
      inProgress: "#3448C5",
      complete: "#20B832",
      sourceBg: "#F5FAFE"
    },
    fonts: {
      default: null,
      "'Fira Sans', sans-serif": {
        url: "https://fonts.googleapis.com/css?family=Fira+Sans",
        active: true
      }
    }
  }
}, (error, result) => {
  if (!error && result && result.event === "success") {
    console.log("Upload successful:", result.info);
  }
});

// Open the widget
document.getElementById("upload-button").addEventListener("click", function(){
  accessibleUploadWidget.open();
}, false);
```

> **TIP**:
>
> :title=Best practices for accessible Upload widgets

> * **Use the accessible theme**: Apply the predefined accessible color palette or ensure custom colors meet WCAG 2.1 AA contrast requirements

> * **Limit source options**: Reduce cognitive load by showing only the sources your users need

> * **Test with keyboard only**: Verify all functionality is accessible without a mouse

> * **Test with screen readers**: Ensure the experience is logical and informative for assistive technology users

> * **Provide clear feedback**: Use the widget's event callbacks to give users confirmation of successful uploads

> * **Use descriptive button text**: Make it clear what the upload button does

> * **Consider mobile users**: The widget automatically adapts to mobile devices with touch-friendly controls

> **READING**:
>
> * Docs: [Upload widget guide](upload_widget)

> * Docs: [Upload widget reference](upload_widget_reference)

> * Demo: [Upload widget demo page](https://demo.cloudinary.com/uw/)

.select-wrapper {
    position: relative;
    width: 200px;
    margin-bottom: 20px;
}
.custom-select {
    position: relative;
    width: 100%;
}
.select-selected {
    background-color: var(--dropdown-menu-bg-color);
    padding: 10px 35px 10px 10px;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
    cursor: pointer;
}
.select-selected::after {
    content: '\25BC';
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
    pointer-events: none;
}
.select-items {
    position: absolute;
    background-color: var(--dropdown-menu-bg-color);
    top: 100%;
    left: 0;
    right: 0;
    z-index: 99;
    border: 1px solid #ccc;
    border-top: none;
    border-radius: 0 0 4px 4px;
    max-height: 300px;
    overflow-y: auto;
    display: none;
}
.select-item {
    padding: 10px;
    cursor: pointer;
}
.select-item:hover {
    background-color: var(--dropdown-background-active-color);
}
.language-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    vertical-align: middle;
}
select {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    padding: 10px 35px 10px 10px;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background-color: var(--dropdown-menu-bg-color);
    cursor: pointer;
}
.select-wrapper::after {
    content: '\25BC';
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
    pointer-events: none;
}

/*
.custom-select {
    position: relative;
    width: 200px;
}
.select-selected {
    background-color: #fff;
    border: 1px solid #ccc;
    padding: 8px;
    cursor: pointer;
    border-radius: 4px;
    display: flex;
    align-items: center;
}
.select-selected:after {
    content: "\25BC";
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
}
.select-items {
    position: absolute;
    background-color: #fff;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 99;
    border: 1px solid #ccc;
    border-top: none;
    border-radius: 0 0 4px 4px;
}
.select-hide {
    display: none;
}
.select-items div {
    padding: 8px;
    cursor: pointer;
    display: flex;
    align-items: center;
}
.select-items div:hover {
    background-color: #f1f1f1;
}
.language-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
}
*/

.select-css-pocs {
	display: block;
	font-size: 16px;
	font-family: sans-serif;
	font-weight: 700;
	color: #3448c5;
	line-height: 1.3;
	padding: .6em 1.4em .5em .8em;
	width: 100%;
	max-width: 100%; /* useful when width is set to anything other than 100% */
	box-sizing: border-box;
	margin: 0;
	border: 1px solid #aaa;
	box-shadow: 0 1px 0 1px rgba(0,0,0,.04);
	border-radius: .5em;
	-moz-appearance: none;
	-webkit-appearance: none;
	appearance: none;
	background-color: #fff;
	/* note: bg image below uses 2 urls. The first is an svg data uri for the arrow icon, and the second is the gradient. 
		for the icon, if you want to change the color, be sure to use `%23` instead of `#`, since it's a url. You can also swap in a different svg icon or an external image reference

	*/

	background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%233448c5%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');

	background-repeat: no-repeat, repeat;

	/* arrow icon position (1em from the right, 50% vertical) , then gradient position*/
	background-position: right .7em top 50%, 0 0;
	/* icon size, then gradient */
	background-size: .65em auto, 100%;
}

/* CSS for demos */

.select-css {
	display: block;
	font-size: 16px;
	font-family: sans-serif;
	font-weight: 700;
	color: #FF5050;
	line-height: 1.3;
	padding: .6em 1.4em .5em .8em;
	width: 100%;
	max-width: 100%; /* useful when width is set to anything other than 100% */
	box-sizing: border-box;
	margin: 0;
	border: 1px solid #aaa;
	box-shadow: 0 1px 0 1px rgba(0,0,0,.04);
	border-radius: .5em;
	-moz-appearance: none;
	-webkit-appearance: none;
	appearance: none;
	background-color: #fff;
	/* note: bg image below uses 2 urls. The first is an svg data uri for the arrow icon, and the second is the gradient. 
		for the icon, if you want to change the color, be sure to use `%23` instead of `#`, since it's a url. You can also swap in a different svg icon or an external image reference

	*/

	background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FF5050%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');

	background-repeat: no-repeat, repeat;

	/* arrow icon position (1em from the right, 50% vertical) , then gradient position*/
	background-position: right .7em top 50%, 0 0;
	/* icon size, then gradient */
	background-size: .65em auto, 100%;
}
/* Hide arrow icon in IE browsers */
.select-css::-ms-expand {
	display: none;
}
/* Hover style */
.select-css:hover {
	border-color: #888;
}
/* Focus style */
.select-css:focus {
	border-color: #FF5050;
	/* It'd be nice to use -webkit-focus-ring-color here but it doesn't work on box-shadow */
	box-shadow: 0 0 1px 3px rgba(255, 80, 80, .7);
	box-shadow: 0 0 0 3px -moz-mac-focusring;
	color: #FF5050; 
	outline: none;
}

/* Set options to normal weight */
.select-css option {
	font-weight:normal;
}

/* Support for rtl text, explicit support for Arabic and Hebrew */
*[dir="rtl"] .select-css, :root:lang(ar) .select-css, :root:lang(iw) .select-css {
	background-position: left .7em top 50%, 0 0;
	padding: .6em .8em .5em 1.4em;
}

/* Disabled styles */
.select-css:disabled, .select-css[aria-disabled=true] {
	color: graytext;
	background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22graytext%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'),
	  linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%);
}

.select-css:disabled:hover, .select-css[aria-disabled=true] {
	border-color: #aaa;

}

table{
    table-layout: fixed;
}

.time_warn{
  color: #FF0000;
  font-size:12px;
}

.instructions{
font-family: Tahoma;
text-align: center;
padding-left: 10%;
padding-right: 10%;
  color: #0c163b;
}

.instructions-large{
  font-family: Tahoma;
  text-align: center;
  font-size:20px;
  padding-left: 10%;
  padding-right: 10%;
  color: #0c163b;
  }

.selectcontainer {
   color: #FF5050;
   font-weight: bold;
   font-size:90%;
}

.selectcontainer-padleft {
  color: #FF5050;
  font-weight: bold;
  font-size:90%;
  padding-left: 15%;
}

.size_value{
  color: #FF5050;
  font-weight: bold;
}

.thumb-img {
  border: solid 6px #aaa;
  border-radius: 6px;
  opacity: 0.5;
}

.thumb-img:hover {
  border: solid 6px #FF8383;
  border-radius: 6px;
  cursor: pointer;
  opacity: 1;
}

.thumb-img.active {
  border: solid 6px #FF5050;
  border-radius: 6px;
  opacity: 1;
}

.art-img, .photo-img  {
  border: solid 6px #aaa;
  border-radius: 6px;
  opacity: 0.5;
}

.art-img:hover,  .photo-img:hover{
  border: solid 6px #f5956c;
  border-radius: 6px;
  cursor: pointer;
  opacity: 1;
}

.art-img.active, .photo-img.active {
  border: solid 6px #FF5050;
  border-radius: 6px;
  opacity: 1;
}

.select_label{
   color: #3448C5;
   font-weight: bold;
}

.select_label1{
  color: #0c163b;
  font-weight: bold;
  font-size: 12px;
}

.select_label2{
  color: #0c163b;
  font-weight: normal;
}

.env_select_label{
   color: #3448C5;
   font-weight: bold;
   padding-left: 15%;
}

.sliders{
  display: inline;
}

.slider_value{
   color: #FF5050;
   font-weight: bold;
}

.slider_label{
   color: #3448C5;
   font-weight: bold;
  padding-left: 15%;
}

.step_number {
        background:  black;
        color:  white;
        width: 24px;
        height: 24px;
        display: inline-block;
        text-align: center;
        line-height: 24px;
        border-radius: 100px;
}

.slidecontainer {
  width: 85%; /* Width of the outside container */
  text-align: center;
float: right;
padding-right: 15%;
}

/* The slider itself */

/* Mouse-over effects */
.slider:hover {
  opacity: 1; /* Fully shown on mouse-over */
}

.slider {
  -webkit-appearance: none;
  width: 100%;
  height: 15px;
  border-radius: 5px;  
  background: #3448C5;
  outline: none;
  opacity: 0.7;
  -webkit-transition: .2s;
  transition: opacity .2s;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 25px;
  height: 25px;
  border-radius: 50%; 
  background: #FF5050;
  cursor: pointer;
}

.slider::-moz-range-thumb {
  width: 25px;
  height: 25px;
  border-radius: 50%;
  background: #FF5050;
  cursor: pointer;
}

.cloudinary-button {
  display: inline-block;
  padding: 15px 25px;
  font-size: 18px;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  outline: none;
  color: #fff;
  background-color: #FF5050;
  border: none;
  border-radius: 15px;
  box-shadow: 0 9px #999;
}

.cloudinary-button:hover {
  background-color: #ff0303;
  cursor: pointer;
}

.cloudinary-button:active {
  background-color: #ff0303;
  box-shadow: 0 5px #666;
  transform: translateY(4px);
}

.fix {
 display: block;
}

.loader {
  position: static;
  margin: auto;
  border: 16px solid #5A616A; 
  border-top: 16px solid #3448C5; 
  border-radius: 50%;
  width: 120px;
  height: 120px;
  animation: spin 2s linear infinite;
} 

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.current-img {
	padding:8px 16px;
}

.demo-btn {
	border: 0px;
	background-color:#FF5050;
	border-radius:30px;
	display:inline-block;
	cursor:pointer;
	color:#ffffff;
	font-size:14px;
	font-weight:600;;
	padding:10px 16px;
	text-decoration:none;
	
}
.demo-btn:hover {
	background-color:#ff0303;
}

a.demo-btn:link, a.demo-btn:visited, a.demo-btn:hover, a.demo-btn:active {
	color: #ffffff;
	text-decoration:none;
}

.demo-btn:active {
	position:relative;
	top:1px;
}

span.mystep {
  background: #FF5050;
  border-radius: 0.8em;
  -moz-border-radius: 0.8em;
  -webkit-border-radius: 0.8em;
  color: #ffffff;
  display: inline-block;
  font-weight: bold;
  line-height: 1.6em;
  margin-right: 5px;
  text-align: center;
  width: 1.6em;
}

.coordinates {
  text-align: center;
  color: #0c163b;
}

.tr_all {
  display:inline-block;
  vertical-align:top;
  margin-left: 3em;
  margin-bottom: 1em;
  text-align: left;
}

.tl_all {
  display:inline-block;
  vertical-align:top;
  margin-right: 3em;
  margin-bottom: 1em;
  text-align: right;
}

.br_all {
  display:inline-block;
  vertical-align:top;
  margin-left: 3em;
  margin-bottom: 1em;
  text-align: left;
}

.bl_all {
  display:inline-block;
  vertical-align:top;
  margin-right: 3em;
  margin-bottom: 1em;
  text-align: right;
}

.options {
  font-size: 18px;
  font-weight: bold;
  color: #0c163b;
}

.coordinate-value {
  color: #FF5050;
  font-weight: bold; 
  text-align: right;  
}

/* Accessible Media Demo Styles */

/* Dark theme support for audio description demo */
[data-theme="dark"] .audio-description-demo {
  border-color: var(--dark-border) !important;
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

[data-theme="dark"] .audio-description-demo h4 {
  color: var(--dark-border) !important;
}

/* Video player demo styles */
#wordHighlight {
  height: 400px;
  padding-top: unset;
}
#wordHighlight > div.vjs-poster > picture > img {
  object-fit: contain;
}

#wordHighlight > div.vjs-poster > picture {
  background: var(--main-content-color);
}
#wordHighlight.video-js {
  background-color: var(--main-content-color);
}

/* Dark theme support for colorblind demo */
[data-theme="dark"] .colorblind-demo {
  background-color: #2d3748 !important;
  border-color: #4a5568 !important;
  color: #e2e8f0 !important;
}

[data-theme="dark"] .colorblind-demo label {
  color: #e2e8f0 !important;
}

[data-theme="dark"] .colorblind-demo select {
  background-color: #4a5568 !important;
  color: #e2e8f0 !important;
  border: 1px solid #718096 !important;
}

[data-theme="dark"] .url-display {
  background-color: #2c5282 !important;
  border: 1px solid #3182ce !important;
}

[data-theme="dark"] .url-display h4 {
  color: #63b3ed !important;
}

[data-theme="dark"] .url-display code {
  color: #e2e8f0 !important;
}

[data-theme="dark"] .tips-section {
  background-color: #744210 !important;
  border: 1px solid #975a16 !important;
}

[data-theme="dark"] .tips-section h4 {
  color: #fbb041 !important;
}

[data-theme="dark"] .tips-section,
[data-theme="dark"] .tips-section ul,
[data-theme="dark"] .tips-section li {
  color: #faf089 !important;
}

/* Dark theme support for text overlay demo */
[data-theme="dark"] .text-overlay-demo label,
[data-theme="dark"] .text-overlay-demo input,
[data-theme="dark"] .text-overlay-demo select {
  --text-color: #e2e8f0;
  --input-bg: #2d3748;
}

/* Dark theme support for OCR text content */
[data-theme="dark"] .ocr-text-content {
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

/* Dark theme support for audio mixing demo */
[data-theme="dark"] .db-status-container {
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

/* Dark theme support for motion demo */
[data-theme="dark"] .motion-demo-container {
  border-color: var(--dark-border) !important;
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

/* Dark theme support for gallery demo container */
[data-theme="dark"] #accessible-gallery-demo {
  background: #2d3748 !important;
  border-color: #4a90e2 !important;
}

[data-theme="dark"] #accessible-gallery-demo h4 {
  color: #4a90e2 !important;
}

[data-theme="dark"] #accessible-gallery-demo p {
  color: #e2e8f0 !important;
}

/* Dark theme support for keyboard controls */
[data-theme="dark"] .keyboard-controls-container {
  border-color: var(--dark-border) !important;
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

[data-theme="dark"] .keyboard-controls-container h4 {
  color: var(--dark-border) !important;
}

[data-theme="dark"] .keyboard-key {
  background: var(--dark-kbd-bg) !important;
  color: var(--dark-kbd-text) !important;
  border-color: #718096 !important;
}

/* Dark theme support for video player demo */
[data-theme="dark"] .video-player-demo {
  border-color: var(--dark-border) !important;
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

[data-theme="dark"] .video-player-demo h4 {
  color: var(--dark-border) !important;
}

[data-theme="dark"] .video-demo-features {
  color: var(--dark-subtext) !important;
}

/* Dark theme support for upload widget demo */
[data-theme="dark"] .upload-widget-demo {
  border-color: var(--dark-border) !important;
  background: var(--dark-bg) !important;
  color: var(--dark-text) !important;
}

[data-theme="dark"] .upload-widget-demo h4 {
  color: var(--dark-border) !important;
}

[data-theme="dark"] .upload-widget-demo > div:last-child {
  color: var(--dark-subtext) !important;
}

/* X-Cld-Error Inspector Tool Styles */
.x-cld-error-inspector {
  max-width: 800px;
  margin: 20px 0;
  padding: 20px;
  border: 1px solid var(--inspector-border, #ddd);
  border-radius: 8px;
  background-color: var(--inspector-bg, #f9f9f9);
  color: var(--inspector-text, #333);
}

.x-cld-error-inspector .input-wrapper {
  margin-bottom: 15px;
}

.x-cld-error-inspector label {
  display: block;
  margin-bottom: 8px;
  font-weight: bold;
  color: var(--inspector-text, #333);
}

.x-cld-error-inspector input[type="text"] {
  width: 100%;
  padding: 10px;
  border: 1px solid var(--inspector-input-border, #ccc);
  border-radius: 4px;
  font-family: monospace;
  font-size: 14px;
  background-color: var(--inspector-input-bg, #fff);
  color: var(--inspector-text, #333);
  box-sizing: border-box;
}

.x-cld-error-inspector button.x-cld-inspect-btn {
  padding: 10px 25px;
  line-height: 1.4;
  background-color: var(--button-background-color);
  font-family: "Inter", Helvetica, Arial, sans-serif;
  color: var(--sign-up-button-color);
  font-weight: 600;
  font-size: 14px;
  text-transform: uppercase;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  transition: filter 0.2s ease;
}

.x-cld-error-inspector button.x-cld-inspect-btn:hover {
  filter: brightness(85%);
}

.x-cld-error-inspector #result-container {
  margin-top: 20px;
}

.x-cld-error-inspector #loading {
  color: var(--inspector-loading, #666);
}

.x-cld-error-inspector .result-success {
  padding: 15px;
  background-color: var(--result-warning-bg, #fff3cd);
  border: 1px solid var(--result-warning-border, #ffc107);
  border-radius: 4px;
  margin-top: 10px;
  color: var(--inspector-text, #333);
}

.x-cld-error-inspector .result-error {
  padding: 15px;
  background-color: var(--result-error-bg, #f8d7da);
  border: 1px solid var(--result-error-border, #dc3545);
  border-radius: 4px;
  margin-top: 10px;
  color: var(--inspector-text, #333);
}

.x-cld-error-inspector .result-ok {
  padding: 15px;
  background-color: var(--result-success-bg, #d4edda);
  border: 1px solid var(--result-success-border, #28a745);
  border-radius: 4px;
  margin-top: 10px;
  color: var(--inspector-text, #333);
}

.x-cld-error-inspector .header-info {
  margin-top: 10px;
  padding: 10px;
  background-color: var(--header-info-bg, #e9ecef);
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  color: var(--inspector-text, #333);
}

.x-cld-error-inspector .header-label {
  font-weight: bold;
  color: var(--inspector-label, #495057);
}

/* Support for explicit dark theme class */
[data-theme="dark"] .x-cld-error-inspector {
  --inspector-border: #4a5568;
  --inspector-bg: #2d3748;
  --inspector-text: #e2e8f0;
  --inspector-input-border: #4a5568;
  --inspector-input-bg: #1a202c;
  --inspector-loading: #a0aec0;
  --inspector-label: #cbd5e0;
  --header-info-bg: #1a202c;
  --result-warning-bg: #744210;
  --result-warning-border: #d69e2e;
  --result-error-bg: #742a2a;
  --result-error-border: #fc8181;
  --result-success-bg: #22543d;
  --result-success-border: #48bb78;
}

/* Image Enhancement Demo Styles */

#image-enhancement-demo {
  max-width: 1200px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid var(--inspector-border, #ddd);
  border-radius: 8px;
  background-color: var(--inspector-bg, #f9f9f9);
}

#image-enhancement-demo h4 {
  color: var(--inspector-text, #333);
  margin-top: 0;
}

#image-thumbs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin-bottom: 30px;
  gap: 10px;
}

.thumb-container {
  text-align: center;
  margin: 10px;
}

.thumb-img {
  cursor: pointer;
  max-width: 150px;
  height: 100px;
  object-fit: cover;
}

.thumb-label {
  font-size: 12px;
  margin-top: 5px;
  color: var(--inspector-text, #333);
}

.demo-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-bottom: 30px;
}

.image-section {
  margin-bottom: 20px;
}

.image-label-wrapper {
  margin-bottom: 10px;
}

.comparison-label {
  color: var(--inspector-text, #333);
}

.comparison-image {
  max-width: 100%;
  height: auto;
  border-radius: 4px;
  display: block;
  cursor: pointer;
}

.comparison-image.original {
  border: 2px solid var(--inspector-border, #ddd);
}

.comparison-image.enhanced {
  border: 2px solid #3448c5;
}

.enhancement-option-wrapper {
  margin-bottom: 15px;
}

.enhancement-option-label {
  display: flex;
  align-items: flex-start;
  cursor: pointer;
  padding: 12px;
  border-radius: 6px;
  border: 2px solid transparent;
  transition: all 0.2s ease;
  background: var(--inspector-input-bg, #fff);
  color: var(--inspector-text, #333);
}

.enhancement-option-label:hover {
  background: var(--dropdown-background-active-color, #f0f0f0);
}

.enhancement-option-label.selected {
  background: var(--inspector-input-bg, #fff);
  border-color: #3448c5;
}

.enhancement-option-label input[type="radio"] {
  margin-right: 10px;
  margin-top: 4px;
}

.enhancement-option-name {
  font-weight: bold;
  margin-bottom: 4px;
}

.enhancement-option-description {
  font-size: 13px;
  opacity: 0.8;
}

#transformation-url {
  margin-top: 20px;
  padding: 20px;
  background-color: #f8fafc;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
}

.url-link {
  text-decoration: none;
}

.url-code {
  background: #f1f5f9;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 13px;
  font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
  color: #0f172a;
  word-break: break-all;
  cursor: pointer;
  display: block;
  border: 1px solid #e2e8f0;
  transition: all 0.2s ease;
}

.url-code:hover {
  background: #e2e8f0;
  border-color: #cbd5e1;
}

#transformation-url > div {
  margin-bottom: 12px;
}

#transformation-url > div:last-child {
  margin-bottom: 0;
}

#transformation-url strong {
  color: #64748b;
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-weight: 600;
}

.thumb-label {
  color: var(--inspector-text, #333);
}

.comparison-label {
  color: var(--inspector-text, #333);
}

/* Dark theme support for Image Enhancement Demo */
[data-theme="dark"] #image-enhancement-demo {
  background-color: #2d3748 !important;
  border-color: #4a5568 !important;
  color: #e2e8f0 !important;
}

[data-theme="dark"] #image-enhancement-demo h4 {
  color: #e2e8f0 !important;
}

[data-theme="dark"] .comparison-image.original {
  border-color: #4a5568;
}

[data-theme="dark"] #transformation-url {
  background-color: #1e293b !important;
  border: 1px solid #334155 !important;
}

[data-theme="dark"] #transformation-url strong {
  color: #94a3b8 !important;
}

[data-theme="dark"] .url-code {
  background: #1e293b;
  color: #94a3b8;
  border-color: #334155;
}

[data-theme="dark"] .url-code:hover {
  background: #334155;
  border-color: #475569;
}

[data-theme="dark"] .enhancement-option-label {
  background: #1a202c;
  color: #e2e8f0;
}

[data-theme="dark"] .enhancement-option-label:hover {
  background: #2d3748;
}

[data-theme="dark"] .enhancement-option-label.selected {
  background: #2d3748;
  border-color: #4a90e2 !important;
}

[data-theme="dark"] .thumb-label {
  color: #e2e8f0 !important;
}

[data-theme="dark"] .comparison-label {
  color: #e2e8f0 !important;
}

 