Shadcn Image Zoom for React and Tailwind
Click to zoom images, SVG, and diagrams.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/image-zoom.jsonpnpm dlx shadcn@latest add https://kit.dev/r/image-zoom.jsonnpx shadcn@latest add https://kit.dev/r/image-zoom.jsonyarn shadcn@latest add https://kit.dev/r/image-zoom.jsonInstall the following dependencies:
bun add react-medium-image-zoompnpm add react-medium-image-zoomnpm install react-medium-image-zoomyarn add react-medium-image-zoomCopy and paste the following code into your project.
"use client";
import type React from "react";
import Zoom, { type UncontrolledProps } from "react-medium-image-zoom";
import { cn } from "@/lib/utils";
export interface ImageZoomProps
extends Omit<React.ComponentProps<"img">, "children"> {
/**
* Custom trigger. Zoom finds the first `img`, `svg`, `[role="img"]`, or `[data-zoom]`.
*/
children?: React.ReactNode;
/**
* Props for `react-medium-image-zoom`.
*/
rmiz?: Omit<UncontrolledProps, "children" | "zoomImg">;
/**
* Image props when zoomed. Defaults to the trigger `src`.
*/
zoomInProps?: React.ComponentProps<"img">;
}
export const ImageZoom = (props: ImageZoomProps) => {
const {
alt = "",
children,
className,
height,
rmiz,
width,
zoomInProps,
...rest
} = props;
const src = typeof rest.src === "string" ? rest.src : undefined;
const zoomImg =
src === undefined && zoomInProps === undefined
? undefined
: { src, ...zoomInProps };
return (
<Zoom wrapElement="span" zoomMargin={20} {...rmiz} zoomImg={zoomImg}>
{children ?? (
<img
alt={alt}
className={cn("h-auto w-full", className)}
height={height}
width={width}
{...rest}
/>
)}
</Zoom>
);
};Add the following styles to your globals.css.
[data-rmiz] {
position: relative;
display: block;
}
[data-rmiz-ghost] {
position: absolute;
pointer-events: none;
}
[data-rmiz-btn-zoom],
[data-rmiz-btn-unzoom] {
display: none;
}
[data-rmiz-content="found"] img,
[data-rmiz-content="found"] svg,
[data-rmiz-content="found"] [role="img"],
[data-rmiz-content="found"] [data-zoom] {
cursor: zoom-in;
}
[data-rmiz-modal][open] {
position: fixed;
width: 100dvw;
max-width: none;
height: 100dvh;
max-height: none;
padding: 0;
margin: 0;
overflow: hidden;
background-color: transparent;
border: 0;
}
[data-rmiz-modal]:focus-visible {
outline: none;
}
[data-rmiz-modal-overlay] {
position: absolute;
inset: 0;
transition: background-color 0.3s;
}
[data-rmiz-modal-overlay="visible"] {
background-color: var(--background);
}
[data-rmiz-modal-overlay="hidden"] {
background-color: transparent;
}
[data-rmiz-modal-content] {
position: relative;
width: 100%;
height: 100%;
}
[data-rmiz-modal]::backdrop {
display: none;
}
[data-rmiz-modal-img] {
position: absolute;
cursor: zoom-out;
image-rendering: high-quality;
transform-origin: 0 0;
transition: transform 0.3s;
}
@media (prefers-reduced-motion: reduce) {
[data-rmiz-modal-overlay],
[data-rmiz-modal-img] {
transition-duration: 0.01ms !important;
}
}Update the import paths to match your project setup.
Anatomy
ImageZoom
└── img | svg | [role="img"] | [data-zoom]shadcn.io Image Zoom is react-medium-image-zoom with shadcn.io overlay tokens (--background). Click the media to FLIP-zoom it; click again or press Escape to close. Zoom buttons are hidden — the media is the control.
The library finds the first img, svg, [role="img"], or [data-zoom] inside the trigger. That is how Anatomy diagrams zoom on docs pages such as Accordion and Action Bar.
Usage
import { ImageZoom } from "@/components/ui/image-zoom";<ImageZoom alt="A desk by a window" src="/image.jpg" />Pass children to zoom SVG or a composed figure. Do not pass src in that case unless you want a raster for the zoomed layer.
<ImageZoom>
<Anatomy title="Accordion anatomy">{/* … */}</Anatomy>
</ImageZoom>Examples
SVG
Any inline SVG zooms. Theme fills (var(--primary), var(--muted)) stay on the clone when they resolve from the document.
Guides
Image Zoom vs Image Cropper
| Image Zoom | Image Cropper | |
|---|---|---|
| Shape | Lightbox / FLIP zoom | Crop handles and viewport |
| Use when | Inspect a photo or diagram | Choose a crop before upload |
Docs anatomy
Wrap the figure on a component page (Accordion, Action Bar, above the live preview). Leave Anatomy examples on the Anatomy page unwrapped so the scene stays inspectable.
The zoomed layer is a live SVG clone, not the Open Graph PNG — RTL and --primary follow the page.
Higher-resolution zoom
For photos, pass zoomInProps (or src plus a larger zoomInProps.src) so the lightbox can decode a sharper file than the inline image.
<ImageZoom
alt="A desk by a window"
src="/desk-800.jpg"
zoomInProps={{ src: "/desk-1600.jpg" }}
/>API Reference
zoomMargin defaults to 20. Remaining zoom behavior is react-medium-image-zoom.
ImageZoom
img, svg, [role="img"], or [data-zoom].| Attribute | Description |
|---|---|
data-rmiz | Root from react-medium-image-zoom |
data-rmiz-content | "found" or "not-found" |
data-rmiz-modal | Native dialog (top layer) |
Accessibility
The zoomed view is a modal dialog. Click the media to open; click it or press Escape to close. Give photos alt. Give SVG an aria-label (Anatomy uses the diagram title).
Keyboard support
| Key | Description |
|---|---|
Escape | Close the zoomed view. |
Click or tap the media to zoom. Scroll the page (wheel) also closes the zoomed view.