About this animation
A product gallery with focal zoom, elastic panning, and color selection, with configurable variants, copy, background color, and fonts.
Why this one
Zoom stays anchored to the touched point. Panning carries momentum into soft boundaries instead of jumping on release. The stage tint and thumbnail indicator follow the same continuous variant index, while product details move out of the way during inspection.
How it works
- Measure the container with
onLayout; stage geometry follows the available space. - Drive bottle position, perspective, radial tint, and thumbnail indicator from one shared variant index.
- Compose exclusive double and single taps simultaneously with pinch and pan. Pinch uses its focal point in stage coordinates.
- Switch the pan from variant browsing to image translation above 1.02×. Apply elastic resistance outside the bounds, then decay within clamps on release.
- Derive stage expansion and detail visibility from zoom. Expose ref methods for host controls and demo playback.
Install
npx expo install @shopify/react-native-skia expo-image react-native-gesture-handler react-native-reanimated react-native-workletsWrap your app in GestureHandlerRootView. For web, initialize CanvasKit before importing this component (for example through Skia's WithSkiaWeb deferred entry). Keep transparent images bundled locally and crop unused transparent margins. No demo images or fonts are required by the reusable component.
Usage
import ProductZoomGallery from './product-zoom-gallery';
<ProductZoomGallery
variants={[
{ name: 'Green', color: '#8FA98F', image: require('./green.png') },
{ name: 'Clay', color: '#C9714F', image: require('./clay.png') },
]}
title="Trail Bottle 24 oz"
onAddToBag={(index) => addVariantToCart(index)}
/>const gallery = useRef<ProductZoomGalleryRef>(null);
<ProductZoomGallery
ref={gallery}
variants={variants}
doubleTapScale={2.4}
maxZoom={3}
springDamping={28}
backgroundColor="#F4F7F6"
textColor="#0F2E2A"
fontFamily="YourInterfaceFont"
titleFontFamily="YourEditorialFont"
monoFontFamily="YourPriceFont"
/>
// From a host button or event handler:
gallery.current?.zoomTo(2.2, 0.5, 0.18);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variants | ProductVariant[] | required | Name, hex color, and image per variant. Empty lists are supported. |
| header | ReactNode | — | Host product-page header. |
| title | string | Product | Product heading. |
| description | string | Insulated steel | Material shown after the active color. |
| price | string | $42.00 | Formatted price. |
| rating | string | 4.8 | Formatted rating. |
| reviewCount | string | (1,284) | Formatted review count. |
| actionLabel | string | Add to bag | Purchase label and accessible name. |
| onAddToBag | (index: number) => void | — | Purchase event. |
| onIndexChange | (index: number) => void | — | Nearest variant changes, on the JS thread. |
| onZoomChange | (zoomed: boolean) => void | — | Browsing/zoomed state changes, on the JS thread. |
| fontFamily | string | system | Interface typeface. |
| titleFontFamily | string | system | Editorial typeface. |
| monoFontFamily | string | system | Price typeface. |
| backgroundColor | string | #F4F7F6 | Page background. |
| textColor | string | #0F2E2A | Text and CTA color. |
| springDamping | number | 28 | Spring damping; stiffness 300 and mass 1. Values below the damping needed for ζ ≥ 0.8 are raised to that minimum. |
| doubleTapScale | number | 2.2 | Double-tap zoom, clamped to maximum. |
| maxZoom | number | 3 | Maximum scale, at least 1. |
| accessibilityLabel | string | Product gallery | Stage description. |
| style | StyleProp<ViewStyle> | — | Root container styling. |
| testID | string | — | Root identifier. |
Ref methods: goTo(index), zoomTo(scale, x = 0.5, y = 0.5), driftTo(x, y), and resetZoom(). Focal coordinates are normalized; drift coordinates are normalized translations, clamped to zoom bounds. goTo also exits zoom.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Skia 2.14.0. On web, defer the Skia module until CanvasKit is ready.
Accessibility
Color thumbnails and the Zoom/Reset button provide touch alternatives to swiping and pinching, with hit areas of at least 44 pt in the supplied demo sizes. Buttons expose roles, labels, and selection state. Product details hidden during zoom are removed from accessibility traversal. Reduced motion fades variant changes, removes perspective and carousel travel, uses immediate host zoom or short transitions, and disables panning inertia. The stage includes a gesture hint; a single tap stops image inertia.
Demo timeline and layout
The demo uses the kit's Tidewell tile and Lucide glyph. Local bottle alpha bounds were measured and cropped to 184×597 px from the supplied images. Icons: Lucide (ISC).
Autoplay: zoom onto the cap at 0.38 s, drift at 0.93 s, reset at 1.73 s, select Clay at 2.33 s, select Butter at 3.98 s; repeat after 5.4 s. Timers are cleaned up on unmount.
Five 56 pt thumbnails fit both widths. Product copy starts at 70%, with a 35.4 pt fitted title, 15 pt description, 22 pt price, and 56 pt CTA at 88%. The small layout has about 25 pt between the price row and CTA. The cap zoom and Clay tint change occur within the first three seconds. The stage uses a real radial shader; no opaque effect block overlays text.




