suiss/uim · Interaction

Image Viewer

A full-screen photo viewer with swipe paging, pinch and double-tap zoom, and swipe down to close.

Image Viewer is an open-source mobile interaction from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.

iOS
Image Viewer (open by default) on iOS in light mode

ImageViewer opens a set of images full screen on a dark media background, starting at the pressed thumbnail. A counter such as 2 of 5 and a close button sit at the top, below the safe area.

Swipe sideways to page between images, with resistance at the first and last image. Pinch to zoom between 1x and 4x around the fingers, double-tap to zoom to 2.5x at the tapped point or back to 1x, and drag to pan while zoomed. Swiping up or down far enough closes the viewer, and a single tap toggles the controls.

Installation

$ pnpm dlx @esuiss/uim@latest add image-viewer

The CLI also adds what it builds on: Icon, Text.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-screens react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/image-viewer.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/interactions/image-viewer.tsx:

uniwind/components/interactions/image-viewer.tsx
import { cn } from "cn"
import { XIcon } from "phosphor-react-native"
import * as React from "react"
import { Platform, View } from "react-native"
import { FadeIn, FadeOut, ReduceMotion } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"
import * as ImageViewerPrimitive from "@/components/primitives/image-viewer"
import { resolveClassName } from "@/components/primitives/use-render"
import { AnimatedView } from "@/components/ui/animated-view"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

type ImageViewerImage = ImageViewerPrimitive.ImageViewerImage

const FullWindowOverlay =
  1. Update the import paths to match your project.

Usage

import { ImageViewer, ImageViewerTrigger, ImageViewerContent } from "@/components/interactions/image-viewer"
<Gallery defaultOpen defaultIndex={1} />

Pass images to ImageViewer, wrap each thumbnail in an ImageViewerTrigger with its index, and render one ImageViewerContent. Give each image an accessibilityLabel. The ImageViewerImage type is exported.

Open state and the current image can be uncontrolled with defaultOpen and defaultIndex, or controlled with open, onOpenChange, index and onIndexChange. useImageViewer returns open, index, count and images.

Examples

Default

iOS
Image Viewer on iOS in light mode

API Reference

ImageViewer

The root that holds the images, open state and current index.

ExtendsReact.ComponentProps<typeof ImageViewerPrimitive.Root>
PropType
About
Called when the viewer opens or closes.
Type
(open: boolean) => void
Default
—
About
Called with the new index after paging.
Type
(index: number) => void
Default
—

ImageViewerTrigger

A pressable thumbnail that opens the viewer at its index.

ExtendsReact.ComponentProps<typeof ImageViewerPrimitive.Trigger>

ImageViewerContent

The full-screen viewer with the pages, counter and close button.

PropType
About
Name of a portal host to render into.
Type
string
Default
—
About
Accessibility label of the close button. Defaults to Close.
Type
string
Default
"Close"
About
Formats the counter from the index and count. Defaults to n of count.
Type
(index: number, count: number) => string
Default
defaultIndexLabel
Type
string
Default
—

Accessibility

  • The viewer is modal for screen readers, and the escape gesture closes it.
  • The current page is adjustable: swiping up or down with a screen reader moves between images. It is labelled by the image's accessibilityLabel, or Image n, with the counter as its value.
  • Triggers have the button role and expose their expanded state.

Notes

  • The viewer fades in and out, and paging and zoom animations respect the system reduce motion setting.
  • Colors use the bg-media and text-on-media tokens, so the viewer stays dark in light mode.
  • On iOS the viewer renders in a full-window overlay so it appears above native screens.