Galeria š·
An image viewer for React (+ Native). It works with any image component - bring your own image component (BYOICā¢)
https://github.com/user-attachments/assets/5062e949-b205-4260-830c-38041cec26db
The video makes Web look a bit weird, but don't worry. Here is why ā it's just because of FlashList's Masonry List, which wraps the views with additional cells breaking z-index. But it isn't a fundamental Galeria issue.
Features
- Shared element transitions
- Pinch to zoom
- Double tap to zoom
- Pan to close
- Multi-image support
- React Native Modal support
- FlashList support
- Clean API
- Web support
- Remote URLs & local images
- New Architecture (Fabric) ā required
- Supports different images when collapsed and expanded
- Works with _any image component_
from react-native
- from solito/image
- from next/image
- from expo-image
- ![]()
on web
- ...etc
For iOS and Android, the implementation uses Swift (ImageViewer.swift) and Kotlin (imageviewer) respectively ā see credits.
Web support is a simplified version of the native experience powered by Framer Motion. It opens one photo or video at a time.
Resources
- @FernandoTheRojo's tweet about Galeria v1
- Watch my talk at App.js Conf about how to build Galeria
Usage
One Image
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'react-native' // works with ANY image component!
const url = 'https://my-image.com/image.jpg'
export const SingleImage = ({ style }) => (
<Galeria urls={[url]}>
<Galeria.Image>
<Image source={{ uri: url }} style={style} />
</Galeria.Image>
</Galeria>
)
Multiple Images
Simply pass an array to urls.
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'react-native' // works with ANY image component!
import localImage from './assets/local-image.png'
const urls = ['https://my-image.com/image.jpg', localImage]
export const MutliImage = ({ style }) => (
<Galeria urls={urls}>
{urls.map((url, index) => (
<Galeria.Image index={index} key={...}>
<Image source={typeof url === 'string' ? { uri: url } : url} style={style} />
</Galeria.Image>
))}
</Galeria>
)
Photos and Videos
iOS, Android, and webPass { type: 'video', source } entries alongside photos in urls. Existing image
URLs and local image assets still work. A typed entry's source accepts a URL
string, a bundled asset such as require('./video.mp4'), or { uri: string }
for remote and local file URIs. Galeria.Item is an alias for
Galeria.Image and accepts the same props.
import { Galeria, type MediaSource } from '@nandorojo/galeria'
import { Image } from 'react-native'
const media: MediaSource[] = [
{ type: 'photo', source: 'https://example.com/photo.jpg' },
{ type: 'video', source: 'https://example.com/video.mp4' },
]
const thumbnails = [
'https://example.com/photo.jpg',
'https://example.com/video-poster.jpg',
]
export const MixedGallery = () => (
<Galeria urls={media} autoPlayVideo={false}>
{thumbnails.map((uri, index) => (
<Galeria.Item index={index} key={uri}>
<Image source={{ uri }} style={{ width: 120, height: 120 }} />
</Galeria.Item>
))}
</Galeria>
)
Videos open paused by default. Set autoPlayVideo to start playback when a video
page becomes active. Playback uses native AVKit controls on iOS, Media3 controls on Android, and the browser's standard video controls on web.
Tap the video to reveal the controls. Playback stops when dismissing the viewer.
On iOS and Android, playback also pauses when leaving the page or putting the app
in the background.
Provide your own thumbnail image, either from your server or generated in your app. Galeria reuses the loaded image from the mounted item as the video's poster and transition image. It does not extract video thumbnails. If the item is not mounted or its image has not loaded, the poster may be unavailable.
Web retains the single-item viewer and its existing close and scroll behavior.
Browser autoplay policies still apply; if playback is blocked, use the play button.
Web uses URL strings or { uri } sources directly, including bundled assets that
your web bundler emits in those forms. Native numeric asset IDs and native file
URIs must be resolved by the caller before passing them to web. Galeria reuses the
caller's image URL as the poster.
Dark Mode
import { Galeria } from '@nandorojo/galeria'
export const DarkMode = () => (
<Galeria urls={urls} theme="dark">
...
</Galeria>
)
FlashList
import { Galeria } from '@nandorojo/galeria'
import { Image, type ImageAssetSource } from 'react-native' // works with ANY image component!
import { FlashList } from '@shopify/flash-list'
import localImage from './assets/local-image.png'
const urls = ['https://my-image.com/image.jpg', localImage]
const size = 100
export const FlashListSupport = () => {
return (
<Galeria urls={urls}>
<FlashList
data={urls}
renderItem={({ item, index }) => {
// you should put this in a memoized component
return (
<Galeria.Image index={index}>
<Image
source={src(item)}
style={{ width: size, height: size }}
/>
</Galeria.Image>
)
}}
numColumns={3}
estimatedItemSize={size}
keyExtractor={(item, i) => item + i}
/>
</Galeria>
)
}
const src = (s) => (typeof s === 'string' ? { uri: s } : s) // š¤·āāļø
Get Index of Currently Shown Image
iOS & AndroidTo get the index of the currently shown image in the image viewer use onIndexChange. It triggers on initial open of the image viewer and when the user scrolls through the images.
<Galeria urls={urls}>
{urls.map((url, index) => (
<Galeria.Image
index={index} key={...}
onIndexChange={(e) => setCurrentIndex(e.nativeEvent.currentIndex)}
>
<Image source={typeof url === 'string' ? { uri: url } : url} style={style} />
</Galeria.Image>
))}
</Galeria>
Hide Blur Overlay
iOS onlyHide the blur overlay that appears behind the image viewer.
<Galeria.Image hideBlurOverlay>
<Image source={{ uri: url }} style={style} />
</Galeria.Image>
Hide Page Indicators
iOS onlyHide the page indicator dots when viewing multiple images.
<Galeria.Image hidePageIndicators>
<Image source={{ uri: url }} style={style} />
</Galeria.Image>
Plain Web Support
Galeria does not use _any_ React Native code on the web. It is a pure React component library.
So you can even use if you want to only use it on web.
import { Galeria } from '@nandorojo/galeria'
const urls = ['https://my-image.com/image.jpg']
export const WebSupport = () => (
<Galeria urls={urls}>
<Galeria.Image>
<img src={urls[0]} width={100} height={100} />
</Galeria.Image>
</Galeria>
)
Solito Image
import { SolitoImage } from 'solito/image'
const urls = ['https://my-image.com/image.jpg']
export const SolitoSupport = () => (
<Galeria urls={urls}>
<Galeria.Image>
<SolitoImage src={urls[0]} />
</Galeria.Image>
</Galeria>
)
Next.js Image
'use client'
import { Galeria } from '@nandorojo/galeria'
import Image from 'next/image'
const urls = ['https://my-image.com/image.jpg']
export const NextJS = () => (
<Galeria urls={urls}>
<Galeria.Image>
<Image
src={urls[0]}
width={100}
height={100}
// edit these props for your use case
unoptimized
/>
</Galeria.Image>
</Galeria>
)
Expo Image
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'expo-image'
const urls = ['https://my-image.com/image.jpg']
export const ExpoImage = () => (
<Galeria urls={urls}>
<Galeria.Image>
<Image source={urls[0]} style={{ width: 100, height: 100 }} />
</Galeria.Image>
</Galeria>
)
Installation
Requirements
- New Architecture (Fabric) ā Galeria v3.0+ requires the new architecture. This means Expo SDK 54+ or React Native 0.79+.
- iOS 16.4+
- Bare RN: set it in
ios/Podfile - Expo: set it via
expo-build-properties
ExpoModulesCore.
yarn add @nandorojo/galeria
or
npm i @nandorojo/galeria
Next.js / Solito
Add @nandorojo/galeria to transpilePackages in your next.config.js.
module.exports = {
transpilePackages: ['@nandorojo/galeria'],
}
Expo
Galeria uses native libraries on iOS and Android, so it does not work with Expo Go. You will need to use a dev client.
After installing it, rebuild your native code:
npx expo prebuild
npx expo run:ios # or npx expo run:android
Credits
- Under the hood, Galeria uses native libraries on iOS and Android.
- On Web, Galeria uses Framer Motion.
- Thanks to Luke Zhao for DynamicTransition
- Thanks to Michael Henry for the iOS Image Viewer
- Thanks to iielse for the Android Image Viewer
- Thanks to Alan for building the Android integration.