About this animation
A photo story with opposing image and text scroll directions, with configurable chapters, background color, column gap, and fonts.
Why this one
Instead of placing a photo above a caption, the columns move toward each other. A real Skia saturation matrix restores color as each photo aligns, while an accent stroke traces its perimeter. The final photograph expands across the gutter and becomes the closing canvas.
How it works
onLayoutmeasures the containing viewport; no global screen dimensions are used.useAnimatedScrollHandlerturns scroll distance into one continuous progress value, from 0 to 3. Chapter centers are 0.2, 1, 1.8, and 2.6; the remaining distance completes the closing frame.- Each chapter's local phase has a ±20% hold, with smoothstep between holds. The photo moves down and its note moves up.
- Headline entrance and exit windows do not overlap. Background images enter over the previous image, with top and bottom dark veils.
- ColorMatrix restores saturation near alignment; the perimeter path draws with the same progress. The last frame grows to the viewport between 2.72 and 3.
- The closing action stays inside the ScrollView. Ref methods drive real scroll offsets; autoplay belongs to the demo and stops when the user takes over.
Install
npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets expo-linear-gradientCopy this folder without demo/. Load Skia's CanvasKit before rendering on web; in Expo use the host app's Skia web loading boundary. Supply local image modules and preload custom fonts before mounting. Icons: Lucide (ISC); see the source attribution in icons.ts.
Usage
import { SplitColumnsScroll } from './split-columns-scroll';
const chapter = {
image: require('./assets/coast.jpg'),
title: 'First light.',
emphasis: 'Keep looking.',
eyebrow: 'COAST',
copy: 'Walk 2 miles. Watch the horizon.',
settings: 'f/2.8 · 1/500 · ISO 100',
location: 'Atlantic coast · 6:12 AM',
};
<SplitColumnsScroll chapters={[chapter, chapter, chapter, chapter]} />;With personality controls and an imperative ref:
const story = useRef<SplitColumnsScrollRef>(null);
<SplitColumnsScroll
ref={story}
chapters={myFourChapters}
accentColor="#E6AD78"
columnGap={8}
photoHeightRatio={0.64}
titleFontFamily="MySerif"
italicFontFamily="MySerifItalic"
ctaLabel="Explore the collection"
onCtaPress={openCollection}
/>;
// Call from an event or a demo timer.
story.current?.scrollToProgress(1.8, 1500);Import useRef from React and SplitColumnsScrollRef from this folder for the second example.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| chapters | readonly SplitColumnsChapter[] | required | Four chapters with image, title, emphasis, eyebrow, copy, settings, location, and optional accent |
| header | ReactNode | none | Brand content; stays left of the viewer close control |
| collectionLabel | string | Field notes | Collection caption |
| closingTitle / closingEmphasis | string | Made to remember. / Every frame. | Closing headline |
| ctaLabel | string | Explore | Closing button and accessible label |
| onCtaPress | () => void | none | Action callback |
| onInteraction | () => void | none | Notify a demo clock to stop |
| onChapterChange | (index: number) => void | none | Active chapter callback on JS |
| backgroundColor / textColor / accentColor | string | #111111 / #F5F5F4 / #F5C26B | Palette |
| columnGap | number | 6 | Gutter in points |
| photoHeightRatio | number | 0.62 | Photo height relative to the viewport |
| fontFamily / titleFontFamily / italicFontFamily / monoFontFamily | string | system | Loaded font family names |
| snapToChapters | boolean | true | Native-only snapping; web keeps continuous wheel scroll |
| accessibilityLabel | string | Photo field notes | Story label |
| style / testID | StyleProp<ViewStyle> / string | none | Root customization |
The ref exposes scrollToProgress(progress, duration?), next(), stop(), and reset(). Default scroll duration is 900 ms; reduced motion uses 200 ms. Reset fades to black before returning to the top.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, Skia 2.14.0. Compatible with the installed Expo Go modules. Supports iOS, Android, and web.
Accessibility
Each chapter has a readable accessibility block and a heading in the scroll content. The next-chapter buttons provide a tap alternative to scrolling with 44-point minimum hit targets. The final action has a button role, a label, and independent press feedback. Reduced motion removes column travel, zoom, and hint displacement; photos transition with 200 ms fades. Custom fonts are optional and no React state changes per animation frame.
Demo timeline
At 150 ms, scroll toward chapter one over 1.3 seconds: the two columns meet, the image gains color, and the frame draws within the first 3 seconds. Later chapters begin every 2.6 seconds. The closing expansion follows the fourth chapter, holds for the CTA, then resets through black. Touch, drag, mouse wheel, and chapter buttons stop the autoplay clock.




