# Image viewer

> Tap a thumbnail and the photo flies out of it into a full-screen viewer, its crop opening up as it lands; pinch or double-tap to zoom at your fingers, drag to pan, swipe between photos, and drag down to send it back into its thumbnail while the background fades. On the web the wheel zooms toward the pointer.

- Type: Component (overlays)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/image-viewer
- Markdown: https://goodui.dev/native/components/image-viewer.md
- Registry item: https://goodui.dev/native/r/image-viewer.json
- Dependencies: react-native-gesture-handler, react-native-reanimated, react-native-svg
- Keywords: react native image viewer, lightbox, photo gallery, pinch to zoom, double tap zoom, shared element transition, swipe down to dismiss, fullscreen image, expo image viewer

## When to use

- Photo galleries, listing and product photos, attachments in a chat, receipts and documents people need to read closely.
- Any thumbnail that should open the full image with zoom.

## When not to use

- Swiping through cards or banners inline: use carousel.
- Story-style full-screen media that advances on its own: use stories.
- A single decorative image that never needs to be larger: use an Image.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/image-viewer.json
npx expo install --fix
```

The component lands in `components/goodui-native/image-viewer/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs icon-button, locale.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
import { View } from "react-native";
import { ImageThumbnail, ImageViewer, type ViewerImage } from "@/components/goodui-native/image-viewer/image-viewer";

const photos: ViewerImage[] = [
  { source: "https://example.com/1.jpg", width: 1200, height: 800, alt: "Living room", caption: "Living room, 24 m²" },
  { source: "https://example.com/2.jpg", width: 800, height: 1200, alt: "Kitchen" },
];

export function Gallery() {
  return (
    <ImageViewer images={photos}>
      <View style={{ flexDirection: "row", gap: 4 }}>
        {photos.map((_, i) => (
          <ImageThumbnail key={i} index={i} style={{ flex: 1, aspectRatio: 1 }} />
        ))}
      </View>
    </ImageViewer>
  );
}
```

## Examples

### Open from your own trigger

```tsx
function FloorPlanButton() {
  const viewer = useImageViewer();
  return <Button onPress={() => viewer.open(0)}>View floor plan</Button>;
}

<ImageViewer images={[plan]}>
  <FloorPlanButton />
</ImageViewer>
```

### Know which photo is on screen

```tsx
<ImageViewer images={photos} onIndexChange={setIndex} onOpenChange={(open, i) => track(open ? "open" : "close", i)}>
```

## API reference

### ImageViewer

`import { ImageViewer } from "@/components/goodui-native/image-viewer/image-viewer";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `images` (required) | `ViewerImage[]` | – | Every image the viewer can swipe through, in order. |
| `children` (required) | `ReactNode` | – | Your layout, with an ImageThumbnail per image (or any trigger that calls `useImageViewer().open`). |
| `onOpenChange` | `(open: boolean, index: number) => void` | – | Called when the viewer opens or closes, with the image it is on. |
| `onIndexChange` | `(index: number) => void` | – | Called when a swipe (or arrow key) moves to another image. |
| `maxZoom` | `number` | `4` | Largest pinch zoom. Default 4. |
| `doubleTapZoom` | `number` | `2.5` | Zoom a double tap jumps to, centered on the tap. Default 2.5. |
| `showCounter` | `boolean` | `true` | "2 of 5" at the top. Default `true` with more than one image. |
| `labels` | `Partial<ImageViewerLabels>` | – | Overrides for the built-in strings (viewer and thumbnails); a GooduiLocaleProvider pack sets them app-wide. |

### ImageThumbnail

`import { ImageThumbnail } from "@/components/goodui-native/image-viewer/image-viewer";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `index` (required) | `number` | – | Which of the viewer's `images` this thumbnail shows and opens. |
| `style` | `StyleProp<ViewStyle>` | – | Size and shape. Give it a width and height or an aspectRatio. |
| `radius` | `number` | `14` | Corner radius. The image grows out of these corners and returns into them. Default 14. |
| `resizeMode` | `"none" \| "center" \| "contain" \| "cover" \| "repeat" \| "stretch"` | `"cover"` | How the thumbnail crops. Default `cover`. |
| `accessibilityLabel` | `string` | – | Screen reader label. Defaults to the image's alt text. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

## Accessibility

- Each ImageThumbnail is a button labelled with the image's alt text and the hint 'Opens the image'.
- The viewer is modal. The image is one element labelled 'alt, 2 of 5' with actions Next image, Previous image and Close; the iOS escape gesture and Android's back button close it.
- Moving to another image announces 'Image 3 of 5'; opening announces the position too.
- A close button sits at the top with a '2 of 5' counter; on the web there are also previous and next buttons, arrow keys move between images and Escape closes.
- Captions show in a quiet card at the bottom and are plain text.

## Motion

- Open: the frame travels from the thumbnail's box and corner radius to the image's fitted box on the smooth spring; the image covers the frame, so the thumbnail's crop opens up exactly as it lands. The backdrop fades in.
- Pinch zooms around the fingers (resisting past 1 and maxZoom, then settling back); double-tap zooms to doubleTapZoom at the tap point or back to fit.
- When zoomed the image pans 1:1, resists past its edges and keeps its momentum on release (decay, clamped to the edges).
- At fit, a sideways drag moves between images 1:1 and settles on the smooth spring with the release velocity; the first and last images resist.
- A vertical drag follows the finger, shrinks the image a little and fades the backdrop; release past 110 pt (or a fast flick) sends it back into the thumbnail of the image on screen, otherwise it springs back.
- Tap toggles the controls. A size that arrives late (no width and height given) springs the frame to the right shape.
- Reduced motion: opening and closing fade instead of flying, and page changes from the keyboard or buttons jump. Pinch, pan and drags still follow the finger because they are direct manipulation.

## Haptics

- tap on the close button (IconButton); zooming, paging and dismissing are direct manipulation and stay silent

## Performance

- All gestures and the open and close flight are worklets on the UI thread; React re-renders only when the image index changes or the controls toggle.
- Only the current image and its two neighbours are mounted. Image sizes are read once per image (Image.getSize) when width and height are not given.

## Notes for AI

- Wrap the thumbnails in ImageViewer and give each ImageThumbnail the index of its image; give the thumbnail a size through style (width plus aspectRatio, or flex).
- Pass width and height on each image when you know them so the opening flight lands on the right shape at once.
- Use the same source for the thumbnail and the viewer; the viewer does not load a separate high resolution file.
- useImageViewer().open(index) opens from any trigger inside ImageViewer; without a thumbnail the image scales in from the center.
- The viewer renders through Portal: ThemeProvider (or a PortalHost) must wrap the app.

## Related

- [Carousel](https://goodui.dev/native/components/carousel.md): A snapping card carousel that follows the finger 1:1, shrinks and dims the neighbours, stretches its pagination pill with the drag, and turns that pill into a timer when it autoplays.
- [Stories](https://goodui.dev/native/components/stories.md): An Instagram-style story viewer that grows out of the tapped avatar: segmented bars fill at the top, tap right or left to step, hold to pause while the chrome steps aside, swipe sideways to the next person with a plain slide, and drag down to send it back into the avatar.
- [Chat bubble](https://goodui.dev/native/components/chat-bubble.md): Message bubbles that group into runs with one tail, carry a receipt that ticks forward (clock, a check that draws itself, two checks, both brightening when read) and spring up out of the composer; plus a TypingIndicator whose dots wave and a Composer whose mic morphs into a filled send arrow as soon as there is text.

## Guidance for AI tools

Image viewer: Tap a thumbnail and the photo flies out of it into a full-screen viewer, its crop opening up as it lands; pinch or double-tap to zoom at your fingers, drag to pan, swipe between photos, and drag down to send it back into its thumbnail while the background fades. On the web the wheel zooms toward the pointer. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
