Skip to content
Native Animation
Scroll Story Product animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add scroll-story-product
Skia

Scroll Story Product

A scroll-driven product story with changing poses and callouts, with configurable product image, chapters, fonts, and text color.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A scroll-driven product story with changing poses and callouts, with configurable product image, chapters, fonts, and text color.

How it works

  1. A single scroll offset is the only source of truth. The chapter index, the progress within the chapter and every animated value derive from it on the UI thread.
  2. The product interpolates between the pose (position, scale, rotation) of consecutive chapters, so it glides from one composition to the next.
  3. Each chapter's background color, photo and accent color fade in over the previous ones, stacked, with a slight parallax.
  4. Drifting motes and speed streaks are drawn with Skia. The streaks grow with scroll velocity.
  5. With snapToChapters, the scroll settles on a chapter boundary when the finger lifts. onChapterChange fires only when the chapter changes, not on every frame.

Install

npx expo install @shopify/react-native-skia expo-image expo-linear-gradient react-native-reanimated react-native-worklets

On web, CanvasKit must finish loading before this component mounts. Call LoadSkiaWeb in your web entry point, then render the application:

import { LoadSkiaWeb } from '@shopify/react-native-skia/lib/module/web';

LoadSkiaWeb().then(() => {
  root.render(<App />);
});

Usage

import ScrollStoryProduct, { type ScrollStoryChapter } from './scroll-story-product';

const chapters: ScrollStoryChapter[] = [
  {
    eyebrow: 'Meet Nimbus',
    title: 'Quiet power.',
    titleAccent: 'Everywhere.',
    backgroundColor: '#0B0F14',
    accentColor: '#7CC4FF',
    pose: { scale: 1 },
    grounded: true,
  },
  {
    title: 'Every detail,',
    titleAccent: 'considered.',
    body: 'Machined aluminum, 40-hour battery.',
    backgroundColor: '#14100B',
    accentColor: '#FFB86B',
    pose: { x: 0.18, scale: 1.4, rotate: -8 },
    callouts: [{ x: 0.62, y: 0.42, label: 'Aluminum shell', side: 'right' }],
  },
];

export function ProductStory() {
  return (
    <ScrollStoryProduct
      product={require('./assets/product.png')}
      productAspectRatio={0.8}
      productAccessibilityLabel="Nimbus headphones"
      chapters={chapters}
      cta={{ label: 'Pre-order — $249', caption: 'Ships in November' }}
    />
  );
}

Props

PropTypeDefaultDescription
productImageSource | numberRequiredTransparent cutout of the product.
productAspectRationumberRequiredWidth / height of the product image.
productAccessibilityLabelstring'Product'Label for the product image.
chaptersScrollStoryChapter[]RequiredOne entry per chapter (see below).
cta{ label: string; caption?: string; onPress?: () => void }undefinedCall to action pinned to the last chapter.
titleFontFamilystringundefinedHeadline font.
accentFontFamilystringundefinedFont for titleAccent (an italic display face works well).
fontFamilystringundefinedFont family for interface text.
numberFontFamilystringundefinedFont for the chapter counter ("01 / 05"); a monospace face keeps it steady.
textColorstring'#F5F3EF'Text color.
snapToChaptersbooleantrueSnap to chapter boundaries when the finger lifts.
onChapterChange(index: number) => voidundefinedCalled when the visible chapter changes.
styleStyleProp<ViewStyle>undefinedStyle applied to the outer container.
durationnumber1500Product entrance duration in milliseconds. Spring physics and reduced-motion timing are unchanged.
testIDstringundefinedRoot ID; ${testID}-scroll, ${testID}-chapter-${index} (zero-based), and ${testID}-cta identify controls. Suffix IDs are omitted when testID is absent.

ScrollStoryChapter

FieldTypeDescription
eyebrowstringSmall label above the headline.
titlestringFirst headline line.
titleAccentstringSecond headline line, in the accent font and color.
bodystringSupporting paragraph.
backgroundColorstringChapter background color.
backgroundImageImageSource | numberFull-bleed scene that dissolves in over the previous chapter.
accentColorstringAccent for this chapter.
backdropWordstringOversized word drifting behind the product.
pose{ x?, y?, scale?, rotate? }Where the product rests. x and y are fractions of width and height from the center; rotate is in degrees.
callouts{ x, y, label, side? }[]Labels anchored at fractions of the screen, pointing left or right.
groundedbooleanShow the soft floor shadow under the product.

Compatibility

  • Expo SDK 58
  • React Native 0.88
  • Reanimated 4.7
  • React Native Worklets 0.13
  • React Native Skia 2.14
  • iOS, Android and web: supported
  • Works in Expo Go because Skia, Reanimated, and Worklets are included in the SDK

Accessibility

  • Each chapter is one accessible element whose label combines its eyebrow, title and body. The product image has its own label and the call to action is a button.
  • With Reduce Motion on, the motes and the product's idle bob and sway are off, and snapping is not animated.