About this animation
A scroll-driven bottle breakdown with separating layers and a cross-section, with configurable chapters, annotations, and fonts.
Why this one
The exploded view is live Skia geometry driven by the finger instead of a pre-rendered video. A real even-odd shell mask reveals the vacuum chamber. Delayed dimension lines draw themselves at rest; the closing spring and seam sweep give the assembly a tactile finish.
How it works
- Measure the container with
onLayout. A five-screenAnimated.ScrollViewproduces one continuous chapter progress,p = scrollY / height. - Stack photos in chapter order, fading each incoming photo over the previous one. Dark top and bottom veils preserve readability.
- Hold poses within 20% of each chapter boundary, then interpolate the cap, gasket, electronics, double wall, and base with smoothstep.
- Render metallic gradients, 18 knurl lines, three LEDs, circuit traces, inner contours, and radial halos directly in Skia. Opening a hole in the shell's clip reveals the inner chamber.
- Reveal callouts 120 ms after resting; withdraw them before motion. Final assembly uses an interruptible spring and a 560 ms seam sweep with a 3% scale pop.
- Keep the final action in the scroll content. The demo drives
goToon a timer, stops on user input, then resets under black.
Install
npx expo install @shopify/react-native-skia expo-image expo-linear-gradient react-native-reanimated react-native-workletsCopy the files listed in piece.json. The demo/ folder is optional and contains the bundled Tidewell art. Follow the Skia web setup for your Expo app and wait for CanvasKit before mounting this component; this catalog uses its withSkia loader. Load your fonts once in the host app.
Usage
import { ExplodedProductScroll } from './exploded-product-scroll';
export function ProductScreen() {
return <ExplodedProductScroll onAction={() => openCheckout()} />;
}import { useRef } from 'react';
import {
ExplodedProductScroll,
type ExplodedProductScrollRef,
type ProductChapters,
} from './exploded-product-scroll';
// Supply five chapters with bundled image sources and your own copy.
export function BrandedStory({ chapters }: { chapters: ProductChapters }) {
const story = useRef<ExplodedProductScrollRef>(null);
return (
<ExplodedProductScroll
ref={story}
chapters={chapters}
fontFamily="YourSans"
titleFontFamily="YourSerif"
italicFontFamily="YourSerifItalic"
monoFontFamily="YourMono"
springDamping={18}
springStiffness={240}
ctaLabel="Reserve yours — $58"
shippingLabel="Ships in March · Free returns"
onAction={() => openCheckout()}
/>
);
}Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
chapters | ProductChapters | DEFAULT_CHAPTERS | Exactly five chapters of product storytelling. |
annotations | readonly [string, string, string, string, string] | DEFAULT_LABELS | Labels ordered as cap, gasket, sensor, shell, and base. |
fontFamily | string | undefined | Family used for body copy, labels, and actions. |
titleFontFamily | string | undefined | Family used for editorial headings. |
italicFontFamily | string | undefined | Italic family used for the second heading line. |
monoFontFamily | string | undefined | Family used for chapter counters. |
ctaLabel | string | 'Explore product' | Final action text. |
shippingLabel | string | '' | Text below the final action. |
onAction | () => void | undefined | Called when the final action is activated. |
onChapterChange | (chapter: number) => void | undefined | Called once each time the nearest chapter changes. |
onInteraction | () => void | undefined | Stops host autoplay on any touch or pointer interaction. |
snapToChapters | boolean | true | Enables chapter snapping on native platforms only. |
entranceDuration | number | 1500 | Bottle and photo entrance duration in milliseconds. |
springDamping | number | 48 | Final assembly spring damping. |
springStiffness | number | 220 | Final assembly spring stiffness. |
style | StyleProp<ViewStyle> | undefined | Optional container styling. |
testID | string | undefined | Optional test identifier. |
ref.goTo(chapter, duration = 900) uses the UI-thread scroll driver; ref.next() advances; ref.reset() returns to chapter one under a short blackout. Demo transitions last 1100 ms, with roughly 2600 ms between chapter starts. The entrance previews the closing seam sweep at 900 ms, within the first three autoplay seconds.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. No extra native dependencies beyond this stack.
Accessibility
Each chapter has a screen-reader block with a heading role and complete copy; callout labels are included in its description. The first chapter offers a 50 pt next action; intermediate chapters offer 44 pt actions, providing a tap alternative to scrolling. The final button is 56 pt tall, with its own press spring. Reduced motion removes the rising entrance, pose travel, rotation, zoom, and closing pop, uses a stationary inspection layout, and fades chapter presentation over 200 ms. Decorative images, visual headings, and dimension graphics do not duplicate the accessible content.
Art and layout notes
Its assembled height is 0.46 H. Exploded poses shrink to 76% and shift to 0.4 W, reserving a right-hand column for labels at 320 pt. Headings use two single-line text nodes with font fitting, body copy fits one line, and pills wrap naturally. No controls occupy the viewer's top-right close area. The shell window and seam light are drawn inside the product material, with no opaque effect block over text or photos.
Icons: Lucide (ISC). The Tidewell demo tile uses the kit's droplet and teal gradient.
Spring damping floors use Reanimated 4.7.1’s default mass of 4 when the configuration does not set a mass. Stiffness and mass remain unchanged.
Customization notes
Set chapters[].accent to theme each chapter, its annotations, and the bottle lighting.




