---
title: Cambio Image
description: A molecular image component featuring physics-based zoom transitions, skeleton loading, mobile pinch-to-zoom, and gesture-driven dismissal
---

<ComponentPreview name="cambio-image-demo" />

This component is based on [Cambio Image by Raphael Salaja](https://cambio.raphaelsalaja.com/).

## Installation

<Installation name="cambio-image" dependencies={["cambio", "lucide-react"]} />

<Admonition type="warning" title="Required CSS">
To ensure the zoomed image sits above all other UI elements (like sticky navbars), add this style to your `globals.css`. This handles the z-index elevation when the component enters the `open` state.

```css
.root {
  isolation: isolate;
}

[data-state="open"] {
  z-index: 999 !important;
}
```

</Admonition>

## Usage

The component mirrors the standard HTML `<img>` API but requires explicit dimensions to calculate the zoom physics correctly.

```tsx
import { CambioImage } from "@/components/ui/cambio-image"

export default function Example() {
  return (
    <CambioImage
      src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop"
      alt="Beautiful mountain landscape"
      width={800}
      height={600}
      motion="smooth"
      className="rounded-lg"
    />
  )
}
```

## Examples

### Grid Gallery

When rendering images in a grid/masonry layout, the browser's stacking context can cause overlapping issues during the closing animation.

Pass the `index` prop (usually from your map function) to ensure the active image always animates above its neighbors.

```tsx
<div className="grid grid-cols-3 gap-4">
  {images.map((img, i) => (
    <CambioImage
      key={img.src}
      src={img.src}
      index={i} // Critical for correct layering
      {...img}
    />
  ))}
</div>
```

<ComponentPreview name="cambio-image-grid-demo" />

### Cropped Thumbnails (aspect)

Source images rarely share the same dimensions, which leaves grids ragged. Pass the same `aspect` to each image to crop the thumbnails to a uniform ratio with `object-cover` — the zoomed popup still reveals the full, uncropped image.

```tsx
<div className="grid grid-cols-3 gap-4">
  {images.map((img, i) => (
    <CambioImage
      key={img.src}
      index={i}
      aspect="4 / 5" // equal-height thumbnails; popup shows full image
      {...img}
    />
  ))}
</div>
```

### Dismiss on Scroll

For a more fluid mobile experience, you can enable dismissOnScroll. This mimics native mobile gallery behavior where scrolling away closes the image.

```tsx
<CambioImage
  src="..."
  dismissOnScroll={true}
  dismissOnImageClick={true} // Optional: click image to close
/>
```

<ComponentPreview name="cambio-image-dismiss-on-scroll-demo" />

<Admonition type="note" title="Scroll Context">
  The `dismissOnScroll` behavior may not function as expected when the component
  is rendered within constrained scroll containers such as demo previews or tab
  panels used above. For the best experience testing this functionality, view
  the [full-screen demo](/view/cambio-image-dismiss-on-scroll-demo) where scroll
  events propagate correctly.
</Admonition>

### Motion Presets

You can customize the animation physics using the motion prop. See the Motion Presets table in the [API Reference](#api-reference) for all available options.

```tsx
<div className="flex gap-4">
  {/* Standard snappy feel */}
  <CambioImage src="/img-1.jpg" motion="snappy" width={400} height={300} />

  {/* Bouncy, playful feel */}
  <CambioImage src="/img-2.jpg" motion="bouncy" width={400} height={300} />
</div>
```

<ComponentPreview name="cambio-image-motion-preset-demo" />

### Close Button (controls)

For a minimal focused-image UI, enable `controls` to render a close button when the image is focused. On desktop the button sits in the top-right of the image; on mobile it floats at the viewport's top-right (outside the image).

```tsx
<CambioImage
  src="..."
  controls
/>
```

<ComponentPreview name="cambio-image-close-button-demo" />

<Admonition type="note" title="Pinch-to-zoom on mobile">
  When `controls` is enabled, `draggable` is coerced to `false` so mobile users
  can pinch to zoom the focused image. Dismiss by tapping the close button,
  clicking the backdrop, or pressing <kbd>Esc</kbd>.
</Admonition>

### Icon-Only Expand Mode

For a cleaner UI with explicit expand/collapse controls, use `showExpandIcon` and `iconsOnlyMode`. This displays floating expand/close buttons instead of relying on click-to-zoom behavior.

```tsx
<CambioImage
  src="..."
  showExpandIcon={true}
  iconsOnlyMode={true}
  dismissible={true}
/>
```

<ComponentPreview name="cambio-image-icons-expand-demo" />

## Notes

### Skeleton Loading

Every thumbnail renders a `Skeleton` placeholder sized to the image's box (via `width`/`height` or `aspect`) until the image loads, so layout stays stable with no shift. Images already decoded from cache clear the skeleton immediately on mount.

### Pinch-to-Zoom

On touch devices, the focused (zoomed) image supports two-finger pinch-to-zoom up to 5× with single-finger panning while zoomed. Swipe-to-dismiss is automatically suspended mid-pinch so an aggressive gesture can't accidentally close the popup, and the image snaps back to 1× when released near its original scale.

### Initial Blur Animation

By default (`enableInitialAnimation={false}`) the image renders immediately, backed by a skeleton placeholder until it loads. Set `enableInitialAnimation` to opt into a blur-to-focus animation driven by an `IntersectionObserver` when the image enters the viewport. Leave it off for images "above the fold" (LCP candidates) to avoid visual delays.

```tsx
{/* Opt into the blur-to-focus entrance */}
<CambioImage src="/below-the-fold.jpg" enableInitialAnimation />

{/* Above-the-fold hero: render immediately */}
<CambioImage src="/hero.jpg" loading="eager" />
```

### Advanced Motion Config

For granular control, you can pass an object to the `motion` prop to configure specific phases of the animation independently:

```tsx
<CambioImage
  motion={{
    trigger: "smooth", // The image expanding
    backdrop: "reduced", // The background fade
    popup: "bouncy", // The final resting state
  }}
/>
```

## API Reference

### Props

| Prop                     | Type                | Default    | Description                                                      |
| ------------------------ | ------------------- | ---------- | ---------------------------------------------------------------- |
| `src`                    | `string`            | `-`        | Required. The source URL.                                        |
| `alt`                    | `string`            | `-`        | Required. Accessibility text.                                    |
| `width`                  | `number`            | `-`        | Required. Intrinsic width.                                       |
| `height`                 | `number`            | `-`        | Required. Intrinsic height.                                      |
| `motion`                 | `Preset \| Config`  | `"reduced"` | Motion preset or custom config.                                 |
| `loading`                | `"lazy" \| "eager"` | `"lazy"`   | Image loading strategy.                                          |
| `index`                  | `number`            | `0`        | Z-index offset for list rendering.                               |
| `aspect`                 | `number \| string`  | `-`        | CSS `aspect-ratio` (e.g. `1`, `"4 / 5"`, `"16 / 9"`) for the thumbnail. Crops it to the ratio with `object-cover`; the zoomed popup still shows the full, uncropped image. Useful for equal-height grids. |
| `dismissible`            | `boolean`           | `true`     | Allow dismissing the zoomed image (disabled when iconsOnlyMode). |
| `dismissOnScroll`        | `boolean`           | `false`    | Close the modal on window scroll.                                |
| `dismissOnImageClick`    | `boolean`           | `false`    | Close by clicking the zoomed image itself.                       |
| `showExpandIcon`         | `boolean`           | `false`    | Display floating expand/close icons.                             |
| `iconsOnlyMode`          | `boolean`           | `false`    | Use only icons for expand/close, disable click-to-zoom behavior. |
| `controls`               | `boolean`           | `false`    | Render a close button when focused (top-right on desktop; top-right of viewport on mobile). Forces `draggable={false}` so mobile users can pinch-to-zoom. |
| `enableInitialAnimation` | `boolean`           | `false`    | Blur-to-focus effect on viewport entry.                          |
| `draggable`              | `boolean`           | `false`    | Allows the image to be dragged (ghost image).                    |
| `className`              | `string`            | `-`        | Additional CSS classes for styling.                              |

### Motion Presets

| Preset    | Description                        |
| --------- | ---------------------------------- |
| `snappy`  | Fast, linear transition.                      |
| `smooth`  | Eased, slower transition.                     |
| `bouncy`  | Spring-physics based animation.               |
| `reduced` | Minimal motion for accessibility. (Default)   |
