This component is based on Cambio Image by Raphael Salaja.
Installation
pnpm dlx shadcn@latest add https://deltacomponents.dev/r/cambio-image.jsonTo 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.
.root {
isolation: isolate;
}
[data-state="open"] {
z-index: 999 !important;
}Usage
The component mirrors the standard HTML <img> API but requires explicit dimensions to calculate the zoom physics correctly.
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.
<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>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.
<CambioImage
src="..."
dismissOnScroll={true}
dismissOnImageClick={true} // Optional: click image to close
/>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 where scroll
events propagate correctly.
Motion Presets
You can customize the animation physics using the motion prop. See the Motion Presets table in the API Reference for all available options.
<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>Notes
Disabling Initial Blur
The component uses an IntersectionObserver to trigger a blur-to-focus animation when the image enters the viewport. For images "above the fold" (LCP candidates), you should disable this to prevent layout shifts or visual delays.
<CambioImage src="/hero.jpg" enableInitialAnimation={false} 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:
<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 | "snappy" | Motion preset or custom config. |
loading | "lazy" | "eager" | "lazy" | Image loading strategy. |
index | number | 0 | Z-index offset for list rendering. |
dismissOnScroll | boolean | false | Close the modal on window scroll. |
dismissOnImageClick | boolean | false | Close by clicking the zoomed image itself. |
enableInitialAnimation | boolean | true | Blur-to-focus effect on viewport entry. |
draggable | boolean | false | Allows the image to be dragged (ghost image). |
Motion Presets
| Preset | Description |
|---|---|
snappy | Fast, linear transition. (Default) |
smooth | Eased, slower transition. |
bouncy | Spring-physics based animation. |
reduced | Minimal motion for accessibility. |