About this animation
A year recap whose persistent capsules form charts and constellations, with configurable chapters, year, brand tile colors, and fonts.
Why this one
Most recap screens replace one card with another. This one preserves every shape across five charts, so the viewer can follow the data as it changes form. The favorite-hour clock adds a personal ritual; the final assembly turns the same data into a keepsake. A staggered opening fan and rolling total establish that continuity immediately.
How it works
- The container measures itself with
onLayout; all chart coordinates use fractions of its width and height. useAnimatedScrollHandlerconverts the scroll offset into one continuous chapter progress from 0 to 4.- Each of the sixty Skia capsules interpolates position, dimensions, rotation, and color between five destinations. Smoothstep transitions leave a twenty-percent rest around each chapter; a small index delay creates a wave.
- Background meshes enter above their predecessors. Editorial text uses separate entry and exit windows so consecutive headlines never overlap.
- Numeric columns advance only on their own carry, fade by distance, and settle on an integer. Word metrics enter with a four-pixel blur.
- The share card and its buttons belong to the final scroll page. Native snapping is optional; web scrolling is continuous.
Install
npx expo install react-native-reanimated react-native-worklets @shopify/react-native-skia expo-blurUse the Reanimated/worklets configuration supplied by your Expo SDK. On web, load CanvasKit before mounting this component using Skia's WithSkiaWeb boundary. The component needs a parent with a bounded height. The fonts are supplied by your app; there are no catalog imports in the reusable files.
Usage
import { YearRecapScroll } from './year-recap-scroll';
<YearRecapScroll
brandName="Your practice"
onShare={() => shareRecap()}
onSaveImage={() => exportRecapImage()}
/>;With custom typography and controls:
const recap = useRef<YearRecapScrollRef>(null);
<YearRecapScroll
ref={recap}
year="2027"
brandName="Your practice"
fontFamily="YourSans"
monoFontFamily="YourMono"
titleFontFamily="YourSerif"
italicFontFamily="YourSerifItalic"
graphScale={0.95}
introDuration={800}
snapToChapters={false}
onChapterChange={(chapter) => trackChapter(chapter)}
/>;
// UI-thread scroll animation with the measured narrative easing.
recap.current?.scrollToChapter(2, 1200);
recap.current?.replayIntro();scrollToChapter(chapter, duration = 0) supports fractional chapters, clamps to 0–4, and is interrupted by touch or drag. replayIntro() replays the fan and first rolling total. Keep autoplay scheduling in the host; the supplied demo advances every 2.6 seconds, fades to black to reset, and stops after interaction.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
chapters | readonly YearRecapChapter[] | Five neutral recap chapters | Copy and figures for the five chart poses; missing entries use defaults |
year | string | "2026" | Year in the header and card |
brandName | string | "Your practice" | Header and summary identity |
brandColors | readonly [string, string] | Pink/violet | Closing sparkle tile colors |
fontFamily | string | System | Interface font |
monoFontFamily | string | System | Counters and tabular figures |
titleFontFamily | string | System | Editorial typeface |
italicFontFamily | string | System | Accent conclusion typeface |
graphScale | number | 1 | Relative graph width |
introDuration | number | 700 | Opening fan duration in milliseconds |
snapToChapters | boolean | true | Chapter snapping on native platforms |
onChapterChange | (chapter: number) => void | — | Nearest chapter callback on the JS thread |
onInteraction | () => void | — | Stop host autoplay after touch or drag |
onShare | () => void | — | Host share action |
onSaveImage | () => void | — | Host image export action |
shareLabel | string | "Share" | Share label and accessible name |
saveLabel | string | "Save image" | Save label and accessible name |
style | StyleProp<ViewStyle> | — | Container style |
testID | string | — | Root test identifier |
YearRecapChapter contains eyebrow, label, value, optional numericValue, headline, and conclusion. The numeric value should match the number embedded in the formatted value (for example, 4812 and "4,812"). Custom chapter text changes the narrative; the chart datasets are the deterministic example in geometry.ts.
Compatibility
Designed for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses Expo Go bundled modules. Android uses a shared BlurTargetView and dimezisBlurViewSdk31Plus; older Android versions receive the module's translucent fallback.
Supports iOS, Android, and web. The host owns image export and platform permissions; the demo shares recap text and offers save feedback without requesting additional native modules.
Accessibility
Each chapter is a labeled header block in scroll order. Metrics use accessible formatted values. The first-page scroll prompt offers a 44-point tap alternative; buttons expose their labels and button roles. Reduced motion disables the opening fan, digit rolling, hint bobbing, and graph morphing: static poses fade across chapters, with 200 ms demo navigation. No parallax, zoom, or trails are used. Press scale uses its own shared value.
Icons: Lucide (ISC). The demo's Aurora sparkle tile uses the repository's approved brand kit.



