Image zoom
Click an image to see it large. Keyboard reachable, Escape closes.
npx shadcn@latest add https://ui.goodtabs.co/r/image-zoom.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/image-zoom
A thumbnail that opens large on click or Enter.
A pair
Two thumbnails side by side, each zoomable.
Usage
import { ImageZoom } from "@/components/ui/image-zoom"
<ImageZoom src="/shot.jpg" alt="The settings page" maxWidth="320px" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | Image URL, used for the thumbnail and the large view. | |
| alt | string | Describes the image for people who cannot see it. | |
| maxWidth | string | Constrain the thumbnail, for example "320px". |
Source
image-zoom/image-zoom.tsx
"use client";
import * as React from "react";
import { Dialog } from "radix-ui";
import { cn } from "@/lib/utils";
type ImageZoomProps = {
src: string;
/** Describe the image for people who cannot see it. */
alt: string;
/** Constrain the thumbnail, for example "320px". */
maxWidth?: string;
className?: string;
};
/**
* Click or press Enter to open the image large over a dimmed page. Escape or
* a click outside closes it. The fade is skipped under reduced motion.
*/
export function ImageZoom({ src, alt, maxWidth, className }: ImageZoomProps) {
const [open, setOpen] = React.useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger
aria-label={`Zoom image: ${alt}`}
className={cn(
"block w-full cursor-zoom-in overflow-hidden rounded-lg border outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
className,
)}
style={{ maxWidth }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={src} alt="" loading="lazy" className="block aspect-3/2 w-full object-cover" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 duration-150 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 motion-reduce:animate-none motion-reduce:duration-0" />
<Dialog.Content
aria-describedby={undefined}
className="fixed inset-0 z-50 grid cursor-zoom-out place-items-center p-4 outline-none duration-150 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 motion-reduce:animate-none motion-reduce:duration-0"
onClick={() => setOpen(false)}
>
<Dialog.Title className="sr-only">{alt}</Dialog.Title>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={src} alt={alt} className="max-h-full max-w-full rounded-lg border object-contain" />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
For AI agents
Hand your agent this page, the markdown at /components/image-zoom/prompt.md, or the Copy AI prompt button above.
When to use
- Screenshots whose detail is lost at thumbnail size
- Article images that deserve a closer look
When not to use
- A gallery with next and previous, which needs a carousel
- Images that are already shown full width
Agent notes
- Client component built on the Radix dialog, so focus is trapped and restored
- The trigger is a button with an accessible name; Escape or a click closes
- The 150ms fade is removed under reduced motion
- Uses the same src for the thumbnail and the large view