About this animation
A scroll-driven drink builder with settling liquid and rolling measures, with configurable chapters, background image, fonts, and accent color.
Why this one
A typical drink page shows a finished photo and an ingredient list. Here, your finger builds the drink: espresso blooms over syrup, milk rises beneath microfoam, and 30 cinnamon flecks land on the surface. A live thermometer and volume ruler make the recipe measurable. Highlights are clipped into the glass and crema, so the material carries the effect.
How it works
- Measure the container with
onLayout. Each chapter spans one container height; the glass occupies 50% of its width and height, from 14% to 64% vertically. - Derive one continuous progress value,
p = clamp(scrollY / height, 0, 4), usinguseAnimatedScrollHandler. All liquid levels, measurements, chapter windows, and photo entrances derive from it. - Fill syrup, espresso, milk, and foam to 8%, 30%, 40%, and 14% of the glass. Smoothstep transitions leave 20% rest zones around each chapter. Clip the fluids to the tapered glass path; blend their edges over 6 points.
- Excite the sinusoidal surfaces when progress changes. A UI frame clock decays the energy at 4/s even after scrolling stops. Scrolling backward withdraws the fluids and reverses the stream and spice positions.
- Reveal chapter headings with separate, nonoverlapping entry and exit windows. Stack blurred local photos using incoming opacity only. The dark veil protects the copy.
- Keep the chapter buttons and final CTA inside the scroll content. Native devices can snap; web stays continuous. Imperative controls animate the real scroll position, so playback and touch use the same progress.
Install
npx expo install @shopify/react-native-skia react-native-reanimated react-native-workletsCopy this folder without demo/ into your application. Follow the Skia web setup for your host application and load CanvasKit before importing this component on web (the catalog uses its withSkia loader). Load any custom fonts in the host app before rendering. Icons: Lucide (ISC); the copied paths are in icons.ts.
Usage
import { LayeredDrinkScroll } from './layered-drink-scroll';
export default function RecipeScreen() {
return <LayeredDrinkScroll onOrder={() => openCheckout()} />;
}With local art, typography, and controls:
const drink = useRef<LayeredDrinkScrollRef>(null);
<LayeredDrinkScroll
ref={drink}
background={require('./assets/counter.jpg')}
headingLabel="Build your seasonal drink"
accentColor="#F59E0B"
waveAmplitude={4}
fontFamily="YourInterfaceFont"
titleFontFamily="YourSerifFont"
italicFontFamily="YourItalicFont"
monoFontFamily="YourMonoFont"
orderLabel="Order for pickup — $5.40"
onOrder={openCheckout}
/>;
// Call from a button handler or a timer, after layout:
drink.current?.goTo(1);Import LayeredDrinkScrollRef as a type for the second example. reset() fades to black, resets the scroll, then fades back in. goTo(chapter, animated = true) accepts chapters 0–4. Both controls remain interruptible by touch.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
chapters | readonly DrinkChapter[] | DEFAULT_CHAPTERS | Five recipe chapters, in syrup / espresso / milk / foam / spice order. |
background | DataSourceParam | undefined | Local background image; never required by the reusable component. |
header | React.ReactNode | undefined | Header content, such as a brand tile and wordmark. |
headingLabel | string | 'Build your drink' | Small heading beneath the optional brand. |
fontFamily | string | undefined | Main interface face. |
titleFontFamily | string | undefined | Editorial headline face. |
italicFontFamily | string | undefined | Editorial italic face. |
monoFontFamily | string | undefined | Tabular metric face. |
accentColor | string | '#F59E0B' | Highlight and order-button color. |
waveAmplitude | number | 5 | Maximum wave amplitude in points. |
snapToChapters | boolean | true | Chapter scroll snapping on native touch devices. |
orderLabel | string | 'Continue' | Final button text. |
onOrder | () => void | undefined | Called when the final button is pressed. |
onChapterChange | ( chapter: number, ) => void | undefined | Called when the nearest chapter changes. |
onInteraction | () => void | undefined | Called on touch or drag; a demo can stop its autoplay. |
style | StyleProp<ViewStyle> | undefined | Outer container style. |
testID | string | undefined | Test identifier. |
duration | number | 1250 | Programmatic chapter scroll duration in milliseconds. Spring physics and reduced-motion timing are unchanged. |
DrinkChapter contains eyebrow, title, subtitle, body, and optional background. The recipe has five chapters and four fixed liquid proportions; the fifth chapter adds spice.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Skia 2.14.0. Uses the installed Expo Go stack. Web requires CanvasKit initialization.
Accessibility
Each chapter heading has the header role. Five labeled chapter buttons provide a tap alternative to scrolling, with 44 × 44 point targets. The 54-point CTA has a button role and a label matching its action. Metrics use tabular digits with invisible sizing copies, per-column carry, and a short transition window to avoid resting between digits.
Reduced motion removes liquid waves, pouring ribbons, falling spice movement, metric rolling, hint bouncing, and animated scroll navigation; headings use a 200 ms fade. There is no parallax, zoom, or velocity trail. The demo stops its timer-driven navigation on touch or drag. Its pickup button confirms locally; connect onOrder to the host checkout for a real order.
Demo timing
The demo begins its espresso chapter at 380 ms and completes navigation by approximately 1.63 s: syrup, crema sweep, and 142°F are visible within the first three seconds. Later chapters advance roughly every 2.6 s. After a final hold, a 200 ms fade hides the reset; playback repeats. Touch stops autoplay.
Layout: the header reserves 66 points on the right for the viewer close control. At 390 × 844 and 320 × 694, the glass stays in the 14–64% band; chapter navigation precedes text at 69%; short-screen headlines use 33-point type, with body copy below. The final CTA sits lower in scroll content and can scroll into view. Contrast comes from a 60% brown veil, top/bottom shading, text shadows, and dark measurement pills. Crema light is inside the liquid/glass clipping geometry, not a colored block over the copy.




