Prerendered at build time · hydrated on load

01srcalt

# Basic usage

Point src at an image and the thumbnail becomes a full viewer: click it to open, scroll or double-click to zoom, pinch on touch, drag to pan. Escape or a backdrop click closes it again.

Artwork 121909597
Click the artwork to open the viewer.
BasicUsage.tsx
import { HanaImgViewer } from 'hana-img-viewer-react'

export default function Example() {
  return (
    <HanaImgViewer
      src="https://pixiv-r2.caelum.moe/121909597.png"
      alt="Artwork 121909597"
    />
  )
}

02previewSrc

# Progressive quality

Keep src light and pass the full-quality file as previewSrc. The overlay opens instantly with the thumbnail, then swaps in the high-res image the moment it finishes loading — no spinner, no layout shift.

Cover that upgrades to a high-res artwork
Open it and watch the image sharpen in place.
ProgressiveQuality.tsx
import { HanaImgViewer } from 'hana-img-viewer-react'

const thumb = '/covers/summer-320w.jpg'
const fullRes = '/covers/summer-2400w.png'

export default function Example() {
  return <HanaImgViewer src={thumb} previewSrc={fullRes} alt="Summer cover" />
}

03openonOpenChange

# Controlled state

Pass open to drive visibility from outside. The viewer never flips state on its own — it calls onOpenChange and waits for your state to change, so your state stays the single source of truth. Controlled mode is decided at mount.

Artwork 129115891
open: false
ControlledState.tsx
import { HanaImgViewer } from 'hana-img-viewer-react'
import { useState } from 'react'

export default function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>Open from the outside</button>
      <HanaImgViewer open={open} onOpenChange={setOpen} src="/photos/garden.png" alt="Garden" />
      <p>open: {String(open)}</p>
    </>
  )
}

05container

# Custom container

Mount the overlay anywhere by passing container. Here it renders inside a scrollable panel in a dialog. While the ref is still null an open request simply waits — the viewer resolves it as soon as the container exists.

The overlay stays inside the dialog's scroll area.
CustomContainer.tsx
import { HanaImgViewer } from 'hana-img-viewer-react'
import { useState } from 'react'

export default function Example() {
  const [container, setContainer] = useState<HTMLElement | null>(null)

  return (
    <section className="scrollable-panel">
      <div ref={setContainer} />
      <HanaImgViewer
        container={container}
        closeOnEscape={false}
        src="/photos/artwork.png"
        alt="Artwork"
      />
    </section>
  )
}

06minZoommaxZoom

# Zoom control

Zoom is always on. Clamp the wheel, pinch, and double-click range with minZoom and maxZoom.

Default zoom rangedefault: 0.5x · 10xZoom clamped between 1x and 3xminZoom: 1 · maxZoom: 3
Open each one and try the scroll wheel or a double-click.
ZoomControl.tsx
{/* Default 0.5x-10x zoom */}
<HanaImgViewer src="/covers/summer.jpg" alt="Cover" />

{/* Wheel, pinch, and double-click clamped to 1x-3x */}
<HanaImgViewer minZoom={1} maxZoom={3} src="/photos/garden.png" alt="Garden" />

07closeOnBackdropClickcloseOnEscapeshowCloseButton

# Close behavior

All three escape hatches are opt-out: backdrop click, Escape, and the corner showCloseButton. The left viewer ignores backdrop clicks; the middle one ignores Escape; the right one hides the close button.

Closes with Escape or the close buttoncloseOnBackdropClick: falseCloses with a backdrop click or the close buttoncloseOnEscape: falseCloses with the backdrop or Escape onlyshowCloseButton: false
Try the wrong close gesture first — nothing happens.
CloseBehavior.tsx
{/* Backdrop clicks are ignored; press Escape or the close button */}
<HanaImgViewer closeOnBackdropClick={false} src="/a.png" alt="A" />

{/* Escape is ignored; click the backdrop or the close button */}
<HanaImgViewer closeOnEscape={false} src="/b.png" alt="B" />

{/* No close button; backdrop and Escape still close */}
<HanaImgViewer showCloseButton={false} src="/c.png" alt="C" />

08transitionDuration

# Transition duration

The open and close FLIP runs for transitionDuration milliseconds. The left one keeps the default 300 ms; the right one stretches the animation to 600 ms.

Default 300ms transitiondefault: 300ms600ms transitiontransitionDuration: 600
Open and close each one — the right viewer animates noticeably slower.
TransitionDuration.tsx
{/* Default 300ms open/close FLIP */}
<HanaImgViewer src="/covers/summer.jpg" alt="Cover" />

{/* Longer, smoother open/close animation */}
<HanaImgViewer transitionDuration={600} src="/photos/garden.png" alt="Garden" />