Skip to content
Native Animation
Recap Story Deck animation preview

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

npx nativeanimation add recap-story-deck
Year in reviewSkia

Recap Story Deck

An annual story deck with kinetic type and ink-ribbon transitions, with configurable slides, colors, navigation labels, and fonts.

$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

An annual story deck with kinetic type and ink-ribbon transitions, with configurable slides, colors, navigation labels, and fonts.

Why this one

Each page has its own color and editorial rhythm. A thick S-shaped ink stroke introduces the next palette while the outgoing type lifts away and the incoming page unfolds vertically. Unlike a fixed annual recap mockup, the deck accepts typed application data and exposes navigation and playback controls.

How it works

  1. A UI-thread frame clock drives line reveals, spring sampling, background drift, and progress. Holding a story freezes this clock, preserving the exact animation position on release. No React state updates run per frame.
  2. Each line has an independent overflow mask. It reveals with an 8-point lift and cubic ease-out over 260 ms; starts are separated by 240 ms. Supporting text begins 200 ms after the final line finishes.
  3. Navigation preserves the outgoing page, lifts it 10 points and fades it over 320 ms. A Skia path with rounded ends and a stroke width of 55% of the container sweeps across the next color in 300 ms. New content unfolds over 400 ms. Backward navigation reverses the sweep.
  4. Four digit reels start at zero, one revolution before their destination. Each samples an independent exact spring solution (damping 14, stiffness 120, mass 1), staggered right to left by 90 ms. After settling at 1,300 ms, the reel clicks from scale 1.06 to 1 over 150 ms. The exact solution lets the clock freeze spring velocity as well as position.
  5. A downward pan follows the finger, shrinks the viewer, and either dismisses it or returns with an interruptible spring. Gesture.Exclusive(LongPress, Tap) separates holding from navigation, with a pan competing in Gesture.Race.
  6. Completed outgoing pages are removed to release their canvases and digit views. Callbacks cross to the JavaScript thread with scheduleOnRN.

Install

npx expo install @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-worklets

Tested dependency targets: Expo SDK 58, React Native 0.88.0, Skia 2.14.0, Reanimated 4.7.1, Gesture Handler 3.3.0, and Worklets 0.13.0. Wrap your app in GestureHandlerRootView. On web, initialize CanvasKit before loading this module using your app's Skia web loader. No assets or project-specific imports are required by the delivered component. Icons: Lucide (ISC); see LUCIDE-LICENSE.txt.

Usage

import { RecapStoryDeck, type RecapSlide } from './recap-story-deck';

const slides: RecapSlide[] = [
  { id: 'intro', type: 'intro', title: 'Your year, in motion' },
  { id: 'days', type: 'statement', lines: ['You showed up', '214 days', 'this year'] },
  { id: 'workouts', type: 'stat', value: 1284, label: 'workouts' },
  { id: 'favorite', type: 'highlight', lines: ['Your favorite', 'hour: 7:00 AM'] },
  { id: 'outro', type: 'outro', buttonLabel: 'Share your year' },
];

<RecapStoryDeck slides={slides} year={2026} onShare={handleShare} />;

The component invokes sharing callbacks; your app creates and shares the actual content. onShare(index) shares a particular story, while the outro calls onShare() for the entire year. Intro and outro default copy derives from year; supply title on those slides to localize it.

const deck = useRef<RecapStoryDeckRef>(null);

<RecapStoryDeck
  ref={deck}
  slides={slides}
  slideDuration={3200}
  loop
  paused={isAppInBackground}
  colors={{
    pages: [
      { background: '#101028', text: '#F7F3EA', accent: '#7CE8B8' },
      { background: '#FFD84D', text: '#101028', accent: '#2346E8' },
    ],
    counter: '#FF6B4A',
    counterText: '#101028',
    counterInset: '#F7F3EA',
    progressTrack: 'rgba(127,127,127,0.28)',
  }}
  timing={{ line: 260, stagger: 240, ribbon: 300 }}
  onClose={dismissRecap}
/>;

// Imperative navigation also works during an in-flight transition.
deck.current?.next();
deck.current?.pause();
deck.current?.play();
deck.current?.replay();

Import useRef from React and RecapStoryDeckRef from this folder for the second example. Supply loaded font families through the font props. The demo supplies Geist ExtraBold, Geist, and Geist Mono; the component otherwise uses platform fonts.

Slides

All pages accept id, eyebrow, subtitle, accessibilityLabel, and colors (a partial per-page palette override).

TypeAdditional fields
introOptional title; displays year above it
statementlines: readonly string[], intended for 2–5 explicit lines
statvalue: number, label: string, optional detail; rounds and clamps to 0000–9999
highlightlines: readonly string[], optional shape: 'star' | 'blob'
outroOptional title and buttonLabel

The palette cycles when there are more slides than page colors. Use unique slide IDs. Empty input produces an empty container. Use finite stat values and concise display copy. No strings contain embedded branding.

Props

PropTypeDefaultPurpose
slidesreadonly RecapSlide[]RequiredStory data
yearnumber2026Intro year and default outro year
slideDurationnumber5000Dwell time in ms, excluding transitions; minimum 100 ms
colorsPartial<RecapColors>Collection palettePage backgrounds, text, accents, counter, screws, progress track
onShare(slideIndex?: number) => void—Story or whole-year share request
onClose() => void—Enables pan and close control; called after dismissal
onSlideChange(index: number) => void—Announced page entry, including initial entry
initialIndexnumber0Initial page, clamped to available data
pausedbooleanfalseExternal pause; overrides imperative play
autoplaybooleantrueAutomatic progression; ignored with reduced motion
loopbooleanfalseRestart after the last page; otherwise remain there
titleFontFamilystringSystemPoster type
fontFamilystringSystemLabels and interface
monoFontFamilystringSystemTabular digit reels
shareLabelstringShare this storyFooter button copy
labels{ previous?, next?, close? }English labelsVisible close and accessible navigation copy
titleScalenumber1Relative display size
timingPartial<{ line, stagger, subtitleDelay, exit, ribbon, enter }>260, 240, 200, 320, 300, 400 msReveal and transition timing
styleStyleProp<ViewStyle>—Container style
testIDstringrecap-story-deckRoot and suffixed control identifiers

Ref methods: next(), prev(), goTo(index), play(), pause(), and replay(). Navigation at the beginning/end is clamped unless looping forward. A new navigation command completes the logical destination of the current transition and transitions from that page. replay() returns to the beginning and clears imperative pause and dismissal.

Compatibility

The root fills its parent and measures its own layout. The parent applies safe-area insets; the component does not add them. Typography and highlight sizes use both width and available story height; content has a 650-point maximum width. Footer controls have a minimum 48×48-point touch area and remain outside the story gesture detector.

Target layouts: 320×568, 390×844, 430×932, 1024×1366, 844×390 landscape, and embedded 300×400. Small layouts reduce poster size. Longer copy, large accessibility font sizes, and custom fonts require review in the host app.

Compatible dependencies are bundled in Expo Go SDK 58. The accompanying Aurora demo has six pages, reports share requests, and loops with a 3.2-second dwell when its autoplay prop is enabled.

Accessibility

Pages announce their complete content upon entry. The story region exposes increment/decrement and escape actions, and the footer has separate “Previous story”, “Next story”, share, and close controls. Reduced motion disables automatic advancement, drift, digit rolling, vertical entry, rotation, shrink, and the ribbon, using 200 ms crossfades instead. Holding pauses typography, background motion, progress, and transitions; imperative/external pause works the same way. Your host should set paused when the screen or application loses focus.