Smooth pinch-to-zoom component for React Native with Apple Photos-style gestures and rubber-band physics
Apple Photos-style zoom component for React Native with pinch, pan, and double-tap gestures.





- 🔍 Pinch to Zoom — Smooth pinch gesture with rubber band effect
- 👆 Double Tap — Tap twice to zoom in/out with configurable scale
- 🖐️ Pan Gesture — Drag zoomed content with momentum and boundary bounce
- 📱 Apple Photos Gallery — Seamless swipe between zoomed images in FlatList
- 🔄 Rubber Band Effect — Natural over-scroll/over-zoom feeling
- 🎯 Focal Point Zoom — Zoom centers on pinch/tap location
- ⚡ 120fps — Silky smooth animations on ProMotion displays
- 📝 TypeScript — Complete type definitions included
| iOS | Android |
https://github.com/user-attachments/assets/9da40463-7b70-46bb-bfe3-eb0ab4f8feb7 | https://github.com/kesha-antonov/react-native-zoom-reanimated/assets/11584712/7e8a572b-8130-4aea-88c7-2ca035a155a1 |
- React Native Zoom Reanimated
- ✨ Features
- Preview
- Table of Contents
- Requirements
- Installation
- Usage
- Examples
- Basic Usage
- Image Gallery with FlatList
- Apple Photos-Style Gallery
- Using the Hook Directly
- API Reference
- Zoom Component Props
- DoubleTapConfig
- ScrollableRef
- Advanced Usage: useZoomGesture Hook
- Zoom Component vs useZoomGesture Hook
- Hook API
- Basic Hook Usage
- Example App
- Platform Support
- Contributing
- Author
- License
| Dependency | Version |
|:----------:|:-------:|
| react-native-reanimated | >= 2.0.0 |
| react-native-gesture-handler | >= 2.0.0 |
Install the library using either Yarn:
``bash`
yarn add react-native-zoom-reanimated
or npm:
`bash`
npm install --save react-native-zoom-reanimated
Make sure you have react-native-reanimated and react-native-gesture-handler installed and configured.
`javascript
import Zoom from 'react-native-zoom-reanimated'
// For Apple Photos-style gallery, also import ScrollableRef type
import Zoom, { ScrollableRef } from 'react-native-zoom-reanimated'
`
> 📁 See the example/ directory for complete working examples.
`jsx
import Zoom from 'react-native-zoom-reanimated'
resizeMode="contain"
style={{ width: deviceWidth, height: imageHeight * deviceWidth / imageWidth }}
/>
`
Basic horizontal gallery with paging:
`jsx`
horizontal
pagingEnabled
renderItem={({ item }) => (
)}
/>
> 📄 Full example: example/ImageGalleryStandalone.tsx
For seamless swipe navigation while zoomed — just like Apple Photos:
`jsx`
parentScrollRef={flatListRef}
currentIndex={index}
itemWidth={deviceWidth + IMAGE_GAP}
>
Features:
- ✅ Swipe between images even while zoomed in
- ✅ Smooth edge-to-scroll transition
- ✅ Auto zoom reset when changing images
- ✅ Gap between images
> 📄 Full example: example/FlatListExample.tsx — complete implementation with all features
For advanced control, use useZoomGesture hook:
`jsx
import { useZoomGesture } from 'react-native-zoom-reanimated'
import { useAnimatedReaction } from 'react-native-reanimated'
const { zoomGesture, contentContainerAnimatedStyle, onLayout, onLayoutContent, zoomOut, isZoomedIn, scale } = useZoomGesture({
minScale: 1,
maxScale: 5,
})
// React to scale changes efficiently in worklet (no JS bridge overhead)
useAnimatedReaction(
() => scale.value,
(currentScale) => {
console.log('Current scale:', currentScale)
}
)
// React to zoom state changes
useAnimatedReaction(
() => isZoomedIn.value,
(isZoomed) => {
console.log('Is zoomed:', isZoomed)
}
)
`
> 📄 Full example: example/UseZoomGestureExample.tsx
| Name | Type | Required | Description |
|-----------------------|------------------------|----------|------------------|
| style | StyleProp | No | Container style |StyleProp
| contentContainerStyle | | No | Content container style |number
| minScale | | No | Minimum allowed zoom scale. Default is 1. Set to 1 to prevent zooming out smaller than initial size. Set to a value < 1 (e.g., 0.5) to allow zooming out to 50% |number
| maxScale | | No | Maximum allowed zoom scale. Default is 4 |(isZoomed: boolean) => void
| onZoomStateChange | | No | Callback fired when zoom state changes. Called with true when zoomed in, false when zoomed out to initial scale |(scale: number) => void
| onZoomChange | | No | Callback fired during zoom gesture with current scale value. Called continuously while pinching, useful for UI updates (e.g., showing zoom percentage). For performance-critical use cases, use useZoomGesture hook with scale SharedValue instead |boolean
| enableGallerySwipe | | No | Enable Apple Photos-style seamless gallery navigation. When zoomed and panning hits horizontal boundary, continued swipe allows scrolling to adjacent images. Default is false |RefObject
| parentScrollRef | | No | Reference to parent FlatList/ScrollView for seamless edge scrolling. When provided with enableGallerySwipe, enables Apple Photos-style continuous swipe: zoomed image pans to edge, then seamlessly scrolls parent list. Compatible with FlatList/ScrollView from react-native, react-native-gesture-handler, and react-native-reanimated |number
| currentIndex | | No | Current index in the parent list (for calculating scroll offset). Required when using parentScrollRef |number
| itemWidth | | No | Width of each item in the parent list (for calculating scroll offset). Required when using parentScrollRef. Usually equals deviceWidth + imageGap |react-native-reanimated
| animationFunction | function | No | Animation function from . Default: withTiming. For example, you can use withSpring instead: https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withSpring |react-native-reanimated
| animationConfig | object | No | Config for animation function from . For example, avaiable options for withSpring animation: https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withSpring#options-object |DoubleTapConfig
| doubleTapConfig | | No | Config for zoom on double tap. See below for details |
| Name | Type | Required | Description |
|---------------|----------|----------|-------------|
| defaultScale | number | No | Fixed zoom scale on double tap. If not set, calculated based on dimensions |number
| minZoomScale | | No | Minimum zoom scale for double tap |number
| maxZoomScale | | No | Maximum zoom scale for double tap |
Type for parentScrollRef. Compatible with FlatList/ScrollView from multiple libraries:
`typescript`
interface ScrollableRef {
scrollToOffset?: (params: { offset: number; animated?: boolean }) => void // FlatList
scrollTo?: (params: { x?: number; y?: number; animated?: boolean }) => void // ScrollView
}
For advanced use cases, use the useZoomGesture hook directly for full control.
> 📄 See example/UseZoomGestureExample.tsx for a complete example.
| Approach | Simplicity | Performance | When to use |
|----------|------------|-------------|-------------|
| Zoom + onZoomStateChange/onZoomChange | ✅ Simple | ⚠️ Via JS bridge | Most use cases |useZoomGesture
| + useAnimatedReaction | ⚠️ More complex | ✅ 120fps, no bridge | Performance-critical apps |
Zoom component uses callbacks (onZoomChange, onZoomStateChange) that communicate via the JS bridge. This is simple to use but may have slight delays on rapid updates.
useZoomGesture hook returns SharedValue objects (scale, isZoomedIn) that update directly in the UI thread. Use useAnimatedReaction to respond to changes without JS bridge overhead — ideal for 120fps animations.
`typescript
interface UseZoomGestureProps {
animationFunction?: typeof withTiming // Animation function (default: withTiming)
animationConfig?: object // Configuration for animation function
minScale?: number // Minimum allowed zoom scale (default: 1)
maxScale?: number // Maximum allowed zoom scale (default: 4)
enableGallerySwipe?: boolean // Enable Apple Photos-style gallery swipe (default: false)
parentScrollRef?: RefObject
currentIndex?: number // Current index in parent list
itemWidth?: number // Width of each item in parent list
doubleTapConfig?: DoubleTapConfig // Double tap zoom configuration
}
interface UseZoomGestureReturn {
zoomGesture: ComposedGesture // Gesture handler to attach to GestureDetector
contentContainerAnimatedStyle: object // Animated styles for the content container
onLayout: (event: LayoutChangeEvent) => void // Container layout handler
onLayoutContent: (event: LayoutChangeEvent) => void // Content layout handler
zoomOut: () => void // Programmatically zoom out
isZoomedIn: SharedValue
zoomGestureLastTime: SharedValue
scale: SharedValue
}
`
`jsx
import { useZoomGesture } from 'react-native-zoom-reanimated'
import { GestureDetector } from 'react-native-gesture-handler'
import Animated from 'react-native-reanimated'
function MyCustomZoomComponent() {
const {
zoomGesture,
contentContainerAnimatedStyle,
onLayout,
onLayoutContent,
zoomOut,
isZoomedIn,
} = useZoomGesture({
doubleTapConfig: { defaultScale: 3, minZoomScale: 1, maxZoomScale: 10 },
})
return (
{/ Your zoomable content /}
)
}
`
`bash`
cd example
yarn install
yarn start:ios # or yarn start:android
The example app demonstrates:
- Basic zoom functionality
- Image gallery with FlatList
- Apple Photos-style seamless navigation
- Using the hook directly
| Platform | Status |
|----------|--------|
| iOS | ✅ Full support |
| Android | ✅ Full support |
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)yarn tsc --noEmit && yarn eslint src/
3. Make your changes
4. Run validation ()git commit -m 'Add amazing feature'
5. Commit your changes ()git push origin feature/amazing-feature`)
6. Push to the branch (
7. Open a Pull Request
Maintained by Kesha Antonov