Skip to content

Quick Start

Wrap any element — the thumbnail can be a plain <img>, next/image, <picture>, or a component from your own design system. ImageView only attaches behaviour and a ref; it never renders an <img> of its own for the trigger.

import { ImageView } from 'react-img-view'
import 'react-img-view/styles.css'
function SingleImagePreview({ image }: { image: { thumb: string; full: string; name: string } }) {
return (
<ImageView src={image.full} alt={image.name} name={image.name}>
<img src={image.thumb} alt={image.name} />
</ImageView>
)
}
Woman holding a vintage camera
camera-portrait.webp

Group collects every ImageView beneath it into one set. Clicking any one opens the viewer positioned on that image, with arrow keys and swipe moving between the rest. No Content is written here — Group supplies the same polished default UI itself.

import { ImageView } from 'react-img-view'
import 'react-img-view/styles.css'
function ImageList({ images }: { images: { src: string; name: string }[] }) {
return (
<ImageView.Group images={images}>
<div className="grid grid-cols-4 gap-3">
{images.map((image, i) => (
<ImageView key={image.src} index={i} {...image}>
<img src={image.src} alt={image.name} className="h-24 w-full object-cover" />
</ImageView>
))}
</div>
</ImageView.Group>
)
}
Woman holding a vintage camera
camera-portrait.webp
Steam rising across a green geothermal landscape
geothermal-landscape.webp
Woman writing in a journal in a sunlit forest
forest-journal.webp
Star-filled night sky above a forest
starry-night.webp

Turning on the counter or the thumbnail strip

Section titled “Turning on the counter or the thumbnail strip”

Both are off by default — for a short set, the arrows say enough. Turn them on for a longer set, where the arrows alone do not say enough:

<ImageView.Group images={images}>
{/* triggers */}
<ImageView.DefaultContent counter thumbnails />
</ImageView.Group>

Use the imperative API when you do not want to manage open and index yourself:

import { ImagePreview } from 'react-img-view/imperative'
function ViewButton({ images }) {
return <button onClick={() => ImagePreview.open({ images, index: 2 })}>View image 3</button>
}

Pass the images and initial index to open(). Calling it again replaces the current viewer. It returns a handle with close(), go(), next(), and prev(); ImagePreview.close() closes whichever function-style viewer is active. The viewer cleans itself up after closing.

An imperative call has no corresponding thumbnail element, so the viewer opens directly without the thumbnail-to-image transition.

Open the viewer from anywhere else in the app — a “View” button in a table row, say — by controlling open and index yourself. Prefer this form when the surrounding component also needs to own those values:

const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
<ImageView.Group
images={images}
open={open}
index={index}
onOpenChange={setOpen}
onIndexChange={setIndex}
>
{/* triggers are optional when you open it programmatically */}
</ImageView.Group>
<Button onClick={() => { setIndex(2); setOpen(true); }}>
View image 3
</Button>

Build your own layout from the same parts DefaultContent uses. Controls and layout regions take asChild; ImageView always reuses its child. Every control publishes data-active / data-boundary / data-disabled for styling.

Keep Content or DefaultContent as a direct child of the main-entry Group. That is the boundary it checks before deciding whether to append the preset.

import { ImageView } from 'react-img-view/primitives'
function CustomViewer({ images }) {
return (
<ImageView.Group images={images}>
{images.map((image, i) => (
<ImageView key={image.src} index={i} {...image}>
<img src={image.src} alt={image.alt} />
</ImageView>
))}
<ImageView.Content className="riv-dialog">
<ImageView.Header className="riv-header">
<ImageView.Close>Close</ImageView.Close>
<ImageView.Title />
<ImageView.Download>Download</ImageView.Download>
</ImageView.Header>
<ImageView.Stage className="riv-stage">
<ImageView.Image />
<ImageView.Prev></ImageView.Prev>
<ImageView.Next></ImageView.Next>
<ImageView.Loading>Loading…</ImageView.Loading>
<ImageView.Error>
{({ retry }) => (
<div>
<p>This image couldn&apos;t be loaded</p>
<button onClick={retry}>Retry</button>
</div>
)}
</ImageView.Error>
<ImageView.Toolbar className="riv-toolbar">
<ImageView.ZoomOut />
<ImageView.ZoomIn />
<ImageView.RotateLeft />
<ImageView.RotateRight />
<ImageView.FitToWindow />
<ImageView.ActualSize />
</ImageView.Toolbar>
</ImageView.Stage>
</ImageView.Content>
</ImageView.Group>
)
}

See Customization for replacing individual buttons, adding your own, and translating every string.

useViewer() works from anywhere inside Group — a custom control, a readout, an analytics hook:

import { useViewer } from 'react-img-view/primitives'
function ZoomReadout() {
const viewer = useViewer()
return <span>{Math.round(viewer.scale * 100)}%</span>
}

See the API reference for everything useViewer() returns and every prop each part accepts.

Key Action
/ Previous / next image
Home / End First / last image
+ / - Zoom in / out
0 Fit to window
1 Actual size (1:1)
R / Shift+R Rotate right / left
Esc Close