Zoom Image
A shadcn-style zoom image component built with Ark UI primitives.
import { ZoomImage } from "@/components/ui/zoom-image";
const IMAGE_CROPPER_SAMPLE =
"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800";
const ZoomImagePreviewDemo = () => (
<ZoomImage
alt="Mountain"
className="max-w-md"
maxZoom={4}
minZoom={1}
src={IMAGE_CROPPER_SAMPLE}
/>
);
export default ZoomImagePreviewDemo;
Installation
npx shadcn@latest add @ark-cn/zoom-imageInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/zoom-image.tsx
"use client";
import type { ComponentProps } from "react";
import {
ImageCropper,
ImageCropperImage,
ImageCropperSelection,
ImageCropperViewport,
} from "@/components/ui/image-cropper";
import { cn } from "@/lib/utils";
export type ZoomImageProps = Omit<
ComponentProps<typeof ImageCropper>,
"children" | "fixedCropArea"
> & {
alt: string;
src: string;
viewportClassName?: string;
};
/** Pinch / wheel zoom and pan preview using Image Cropper with a fixed full-viewport crop (no resize handles). */
export const ZoomImage = ({
alt,
className,
src,
viewportClassName,
...props
}: ZoomImageProps) => (
<ImageCropper
fixedCropArea
className={cn(
"max-w-none [--cropper-overlay-color:transparent]",
className,
)}
{...props}
>
<ImageCropperViewport
className={cn(
"relative aspect-video w-full overflow-hidden rounded-lg bg-muted",
viewportClassName,
)}
>
<ImageCropperImage alt={alt} src={src} />
<ImageCropperSelection className="cursor-grab border-transparent data-dragging:cursor-grabbing" />
</ImageCropperViewport>
</ImageCropper>
);
Update import aliases to match your project setup.
Usage
import * as ZoomImage from "@/components/ui/zoom-image"Read exported parts in src/components/ui/zoom-image.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Preview
import { ZoomImage } from "@/components/ui/zoom-image";
const IMAGE_CROPPER_SAMPLE =
"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800";
const ZoomImagePreviewDemo = () => (
<ZoomImage
alt="Mountain"
className="max-w-md"
maxZoom={4}
minZoom={1}
src={IMAGE_CROPPER_SAMPLE}
/>
);
export default ZoomImagePreviewDemo;
Button zoom
1.00×
import { ZoomInIcon, ZoomOutIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ZoomImage } from "@/components/ui/zoom-image";
const IMAGE_SAMPLE =
"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800";
const ZoomImageButtonZoomDemo = () => {
const [zoom, setZoom] = useState(1);
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex items-center gap-2">
<Button
aria-label="Zoom out"
variant="outline"
size="icon-sm"
onClick={() => setZoom((z) => Math.max(1, z - 0.25))}
>
<ZoomOutIcon aria-hidden className="size-4" />
</Button>
<span className="min-w-12 text-center font-mono text-muted-foreground text-xs tabular-nums">
{zoom.toFixed(2)}×
</span>
<Button
aria-label="Zoom in"
variant="outline"
size="icon-sm"
onClick={() => setZoom((z) => Math.min(5, z + 0.25))}
>
<ZoomInIcon aria-hidden className="size-4" />
</Button>
</div>
<ZoomImage
alt="Mountain"
className="max-w-md"
maxZoom={5}
minZoom={1}
zoom={zoom}
onZoomChange={(e) => setZoom(e.zoom)}
src={IMAGE_SAMPLE}
/>
</div>
);
};
export default ZoomImageButtonZoomDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
ZoomImage
| Prop | Type | Description |
|---|---|---|
| alt | string | Required image alt text. |
| src | string | Required image URL. |
| viewportClassName? | string | Class on the viewport wrapper. |
See the ARK UI documentation for the full API.