
# Galeria 📷

An image viewer for React (+ Native). **It works with any image component - bring your own image component (BYOIC™)**

<!-- <video width="300" src="https://github.com/nandorojo/galeria/assets/13172299/5e915a75-bd40-410f-99fb-5df644ce96ad" ></video> -->


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](https://x.com/FernandoTheRojo/status/1898108643758743882)'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
  - This lets you show smaller thumbnails with higher resolution expanded images
- Works with _any image component_
  - `<Image />` from `react-native`
  - `<SolitoImage />` from `solito/image`
  - `<Image />` from `next/image`
  - `<Image />` from `expo-image`
  - `<img />` on web
  - ...etc

For iOS and Android, the implementation uses Swift (`ImageViewer.swift`) and Kotlin (`imageviewer`) respectively – see [credits](#credits).

Web support is a simplified version of the native experience powered by Framer Motion. It currently supports a single image at a time.

## Resources

- [@FernandoTheRojo's tweet about Galeria v1](https://x.com/FernandoTheRojo/status/1898102430379790606)
- [Watch my talk at App.js Conf](https://www.youtube.com/watch?v=mG1Lv-RWds8) about how to build Galeria
  - "Don't be afraid to build a native library"

## Usage


### One Image

```tsx
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`.

```tsx
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>
)
```

### Dark Mode

```tsx
import { Galeria } from '@nandorojo/galeria'

export const DarkMode = () => (
  <Galeria urls={urls} theme="dark">
    ...
  </Galeria>
)
```

### FlashList

```tsx
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 & Android*

To 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. 

```tsx
<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 only*

Hide the blur overlay that appears behind the image viewer.

```tsx
<Galeria.Image hideBlurOverlay>
  <Image source={{ uri: url }} style={style} />
</Galeria.Image>
```

### Hide Page Indicators
*iOS only*

Hide the page indicator dots when viewing multiple images.

```tsx
<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 `<img />` if you want to only use it on web.

```tsx
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

```tsx
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

```tsx
'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

```tsx
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+**

For iOS 16.4+ deployment target:
- Bare RN: set it in `ios/Podfile`
- Expo: set it via [`expo-build-properties`](https://docs.expo.dev/versions/latest/sdk/build-properties/)

Expo SDK 56 requires iOS 16.4+ for `ExpoModulesCore`.

```bash
yarn add @nandorojo/galeria

# or

npm i @nandorojo/galeria
```

### Next.js / Solito

Add `@nandorojo/galeria` to `transpilePackages` in your `next.config.js`.

```tsx
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:

```bash
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](https://github.com/lkzhao/DynamicTransition) for DynamicTransition
- Thanks to [Michael Henry](https://github.com/michaelhenry/ImageViewer.swift) for the iOS Image Viewer
- Thanks to [iielse](https://github.com/iielse/imageviewer) for the Android Image Viewer
- Thanks to [Alan](https://github.com/alantoa) for building the Android integration.

<img width="1728" alt="Screenshot 2024-05-23 at 1 02 03 PM" src="https://github.com/nandorojo/galeria/assets/13172299/d43f4d04-3510-47fa-8c1d-93cb01644d38">
