Skip to content

API Reference

Grouped by what you import: components, hooks, types, and the entries that ship separately.

A thumbnail that opens the viewer. It reads its surroundings and behaves accordingly:

  • Inside a Group it is a trigger. The group owns the image set, the open state and the chrome, so every ImageView under it opens the same viewer and pages between its siblings.
  • On its own there is nothing to share with, so it supplies a Group and the default UI around itself. That is the one-line case.

Growing a single image into a gallery is therefore additive — wrap the existing ImageViews in a Group; nothing gets renamed.

ImageItem plus:

Prop Type
children ReactElement The element that opens the viewer. Always child-based: ImageView renders its only child and does not accept an asChild prop.
index number Position in the set. Registration order is used when omitted — pass it explicitly for virtualised lists or anything where mount order and visual order can diverge.
disabled boolean
container / labels / renderImage same as Group Only read when there is no Group above, since that is the only time this component owns a viewer. Passing them inside a Group warns in development.

It is also the namespace: every part hangs off it as a static (ImageView.Group, ImageView.Content, …), and each is a named export too if you would rather import them individually and let the bundler drop what you never render. react-img-view/primitives exports the same shape under the same names, so a custom interface is written the same way — see Other entry points.

Prop Type Default
images ImageItem[] triggers’ own registration order ImageItem objects. Omit it and list ImageViews as children instead.
open / defaultOpen boolean defaultOpen = false Controlled / uncontrolled.
onOpenChange (open: boolean) => void
index / defaultIndex number defaultIndex = 0
onIndexChange (index: number) => void
container HTMLElement | null document.body Portal target — see Installation for when to point this at a themed ancestor instead.
extensions Extension[] [] See Extensions below.
labels Partial<ViewerLabels> stable English defaults Every user-facing string. See ViewerLabels.

The <dialog>, portalled. Renders nothing extra — pass children to compose your own layout, or omit it entirely and let Group supply DefaultContent.

With the main entry, keep Content or DefaultContent as a direct child of Group; only that level is inspected when deciding whether to append the preset. The primitives Group never appends it.

Header, Toolbar, Footer — plain positioning containers, all asChild-capable <div>s with no behaviour of their own.

The gesture surface. Image, Prev/Next, Loading, Error are meant to live inside it — the toolbar’s position: absolute in the preset is anchored to the stage specifically so anything placed below it (a caption, Thumbnails) doesn’t push the toolbar out from under the picture.

Renders the current slide and its immediate neighbours (so a page turn never waits on a mount mid-gesture). overscan?: number controls how many slides stay mounted either side of the current one (default 1).

renderImage replaces the full-size image without adding every native image attribute to ImageItem. It receives the current item, index, whether that slide is current, and the library’s assembled imageProps:

<ImageView.Image
renderImage={({ item, imageProps }) => (
<picture>
<source srcSet={`${item.src}.webp`} type="image/webp" />
<img referrerPolicy="no-referrer" {...imageProps} />
</picture>
)}
/>

The final DOM must contain an <img> with imageProps spread onto it. Those props carry the source, sizing styles, loading/error handlers and the plumbing used by transforms and retry. To keep the preset chrome, pass the same renderer to <ImageView.DefaultContent renderImage={...} />.

Part Renders when
<ImageView.Loading> status === 'loading'
<ImageView.Error> status === 'error'. Children may be a render function: {({ retry }) => …}.
<ImageView.Title> Always — current image’s name, falling back to alt.
<ImageView.Counter> Always, once rendered — not in DefaultContent by default. "3 / 8", or a render function {({ index, total }) => …}.

Every control below is a <button> (or <a> for Download) accepting asChild, standard button/anchor props, and publishing data-active / data-boundary / disabled where relevant.

Component data-image-view-control Behaviour
Close close api.close()
Prev / Next prev / next api.prev() / api.next(); data-boundary and disabled at either end of the set
ZoomIn / ZoomOut zoom-in / zoom-out api.zoomBy(1.4) / api.zoomBy(1/1.4); disabled at the zoom ceiling/floor
RotateLeft / RotateRight rotate-left / rotate-right api.rotate(∓90)
FitToWindow fit api.fit(); data-active when scale === fitScale
ActualSize actual-size api.actualSize(); data-active when scale === 1
Download download An <a> with href/download set from the current image

FitToWindow and ActualSize are deliberately two buttons, not a segmented control — a segmented control asserts one option is always selected, but a mid-zoom image has neither active.

Opt-in — never rendered by DefaultContent automatically.

Prop Type Default
mode 'auto' | 'always' | 'never' 'auto'

auto renders nothing for a single image, and hides under a 640px viewport via CSS regardless of image count (a strip costs roughly 15% of a phone’s height for little benefit at that scale). always overrides both.

What Group renders automatically when you don’t supply your own Content. Also usable explicitly, e.g. to turn on counter/thumbnails without composing the rest by hand:

Prop Type Default
counter boolean false
thumbnails boolean | 'auto' | 'always' | 'never' falsetrue means 'auto'
renderImage same as Image

The default error is deliberately compact: visible error copy and an icon-only retry button. Its accessible name and every other user-facing string come from labels on Group.

Returns the live ViewerApi — safe to call from any component inside Group.

interface ViewerApi {
readonly index: number
readonly total: number
readonly open: boolean
readonly transform: Transform // { scale, x, y, rotation }
readonly scale: number
readonly rotation: number
readonly fitScale: number
readonly canZoomIn: boolean
readonly canZoomOut: boolean
readonly canPrev: boolean
readonly canNext: boolean
readonly status: 'idle' | 'loading' | 'ready' | 'error'
zoomTo(scale: number, options?: { origin?: { x: number; y: number }; immediate?: boolean }): void
zoomBy(factor: number): void
fit(): void
actualSize(): void
rotate(degrees: number): void
go(index: number): void
next(): void
prev(): void
close(): void
retry(): void
}

scale/rotation/pan position are deliberately not controllable props — they change every frame during a gesture, and making them controlled would push a 60fps render loop onto every consumer. Read them from useViewer(); drive them with the methods above.

Returns the resolved ViewerLabels — the caller’s overrides already merged over the defaults. Use it in custom controls so they localise from the same labels prop the built-ins do.

interface ImageItem {
src: string
alt?: string
/** Shown by Title; falls back to alt. */
name?: string
/** Skips a measurement round-trip and stops the layout jumping on load. */
width?: number
height?: number
/** Defaults to src. Set when the download should serve a different file. */
downloadUrl?: string
}

Every string the library can render, in one record. errorTitle is the only one DefaultContent renders as visible text — the rest are aria-labels on otherwise icon-only controls, so leaving them untranslated is an accessibility problem, not a cosmetic one: a screen reader announces them verbatim whatever the page’s lang says.

Left unset, Group always uses stable English defaults. This keeps server and hydrated markup identical and leaves language choice to the application. Pass labels to Group (or to <ImageView>); overrides merge field by field, so overriding one key leaves the English defaults for the rest.

interface ViewerLabels {
viewer: string // aria-label on the dialog
close: string
prev: string
next: string
zoomIn: string
zoomOut: string
rotateLeft: string
rotateRight: string
fitToWindow: string
actualSize: string
download: string
retry: string
thumbnails: string // aria-label on the thumbnail strip
thumbnailAt(index: number): string
errorTitle: string // the one string rendered as visible text
loading: string
}

en is exported from the main and primitives entries. Simplified Chinese is opt-in and adds nothing to the default bundle:

import zhCN from 'react-img-view/locales/zh-CN'
function ChineseViewer({ images }) {
return <ImageView.Group images={images} labels={zhCN} />
}

The narrow escape hatch for keyboard behaviour composition can’t reach. Anything that needs to render belongs in the tree, while pointer gestures stay owned by Stage’s tested state machine.

interface Extension {
name: string
onKeyDown?(event: KeyboardEvent, api: ViewerApi): boolean | void
}

Return true to mark the event consumed. Pass an array via Group’s extensions prop.

The same parts under the same names, without the preset. Both Group and ImageView are the headless halves of the components the main entry exports: Group never injects DefaultContent, and ImageView is only a trigger, so it requires a Group rather than standing one up. That fallback is what pulls the preset in, which is why it is not here.

Imported and composed exactly like the main entry — same names, same shape, only the import path differs:

import { ImageView } from 'react-img-view/primitives'
function Gallery({ 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.Group>
)
}

Import ImagePreview from this entry to open the viewer from a function call. It adds nothing to the regular bundle:

import { ImagePreview } from 'react-img-view/imperative'
const viewer = ImagePreview.open({ images, index: 0 })
viewer.go(2)
viewer.next()
viewer.prev()
viewer.close()

open() takes an options object with images: ImageItem[] and an optional initial index. Calling it again replaces the current viewer. An empty set stays closed, and an out-of-range index is adjusted to the nearest valid value. The viewer cleans itself up after it is closed from the handle, the preset UI, or Escape. Call it only in browser code such as an event handler or Effect.

open() option Type Default
images ImageItem[] Images to open.
index number 0 Initial image, clamped to the available range.
content ReactNode preset A custom composition built from ImageView.Content and its parts.
counter boolean false Show the preset counter.
thumbnails boolean | 'auto' | 'always' | 'never' false Show the preset thumbnail strip.
renderImage function Same renderer accepted by ImageView.Image.
container / extensions / labels same as Group Forwarded to the managed Group.

The returned handle controls only the viewer created by that call; after a later open(), an old handle is harmless. ImagePreview.close() closes the currently active function-style viewer. Inside custom content, useViewer() still reads state and controls zoom or rotation. A function call has no thumbnail position, so it opens directly without the thumbnail-to-image transition.

Passing content replaces the entire preset, so counter, thumbnails, and renderImage apply only when the preset is used. With custom content, compose the corresponding parts and image renderer inside it instead.

The gesture math and state machine, exported for anyone building past what the React layer offers:

import { fitScale, tuning } from 'react-img-view/core'

This entry has zero React dependency and tree-shakes independently; see the source in src/core/ for why it stays that way.