About this animation
An annual recap that assembles into a touch-responsive bento poster, with configurable stats, ribbon text, colors, and fonts.
Why this one
The final recap becomes a physical object: five spring-driven tiles settle into place, mechanical digits arrive with them, and a ribbon passes behind the upper corner and in front of the lower corner. Soft holographic light follows the same tilt as the card.
How it works
- Five staggered springs drive both native text and tile artwork. The measured defaults are 110 ms apart, with damping 15, stiffness 170, and explicit mass 1.
- Each tile has one animated native view containing its background, text, and clipped Skia artwork. A four-point star anchors the small accent tile.
- All decoration shares one fixed-size Skia Canvas. A clipped ribbon passes behind the upper-right corner and in front of the lower-left corner; its path reveals over 700 ms.
- Ribbon glyphs are rasterized once into a 3× atlas. One animated distance moves the sprites along a precomputed track at 40 points per second, with no React component per character. Geist ExtraBold supplies the default face; ribbonFont replaces it.
- Gesture Handler updates a normalized tilt shared value. Perspective is 900, rotation is limited to 10 degrees, and release returns through a spring. Idle sway has an eight-second period.
- Sharing is a callback. The component never captures a screenshot or opens a share sheet.
Install
npx expo install @expo-google-fonts/geist @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsUse a GestureHandlerRootView at the app root and the Reanimated/worklets configuration required by your Expo SDK. For web, initialize CanvasKit before mounting this component, following Skia's web setup. No additional native modules are required beyond this project's Expo Go stack.
Usage
import { RecapSharePoster } from './recap-share-poster';
<RecapSharePoster
year={2026}
handle="@alex.moves"
stats={{
main: { value: '1,284', label: 'workouts' },
streak: { value: '42 days', label: 'Longest streak' },
time: { value: '6:30 AM', label: 'Favorite hour' },
top: { value: 'Strength', label: 'Your #1' },
}}
onShare={() => openYourShareFlow()}
/>Personalize the typography, ribbon, colors, and pace:
<RecapSharePoster
ref={posterRef}
fontFamily="YourInterfaceFont"
displayFontFamily="YourHeavyFont"
monoFontFamily="YourNumberFont"
ribbonFont={require('./assets/your-heavy-font.ttf')}
ribbonText="2026 ✦ KEEP GOING ✦"
ribbonSpeed={28}
colors={{ ribbon: '#7CE8B8', ribbonInk: '#101028' }}
onShare={shareYourData}
/>
// posterRef.current?.replay();The component uses 78% of the container width when height permits, caps tablet enlargement, and preserves 9:16 when height requires a smaller card. The parent supplies safe-area insets. Numeric display strings use compact mechanical columns; keep labels and values concise for a story-sized layout.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
stats | RecapPosterStats | Four example fitness stats | main, streak, time, and top, each with value and label |
year | number | 2026 | Header year |
handle | string | @you | Footer username |
brand | ReactNode | None | Host-owned brand tile and wordmark |
colors | Partial<RecapPosterColors> | Collection palette | Paper, ink, tile colors, on-dark ink, ribbon, button, and holographic colors |
ribbonText | string | Year followed by ✦ | Repeated ribbon copy |
ribbonSpeed | number | 40 | Points per second; zero freezes lettering |
tiltEnabled | boolean | true | Touch tilt and idle sway |
onShare | () => void | None | Host share action |
shareLabel | string | Share | Button text and accessible label |
yearLabel | string | My | Header prefix |
accessibilityLabel | string | Generated summary | Override the full poster description |
fontFamily | string | System | Interface copy |
displayFontFamily | string | Interface font | Header type |
monoFontFamily | string | System | Rolling values |
ribbonFont | DataSourceParam | Geist ExtraBold | Optional font asset for ribbon lettering |
staggerMs | number | 110 | Time between arrivals |
ribbonRevealMs | number | 700 | Ribbon reveal time |
swayPeriodMs | number | 8000 | Ambient sway cycle |
style | StyleProp<ViewStyle> | None | Root container styling |
testID | string | recap-share-poster | Root identifier; button appends -share |
Ref methods: play(), replay(), and tiltTo(x, y) for a host-controlled presentation pose in the range −1 to 1. Assembly starts on mount. The demo owns the autoplay loop and simulates left/right poses, then rests for two seconds before replaying.
Compatibility
Expo Go includes the required native modules in this project's SDK.
Accessibility
The poster is exposed as one image whose label summarizes the year, all four values and labels, and the handle. The separate Share button has a minimum 48-point target and a stable test identifier. Reduced motion uses a 200 ms fade, stationary digits and ribbon, and disables tilt and ambient sway. Tilt changes decoration only; every statistic and the Share action remain available without dragging.
Demo icon: Lucide (ISC), supplied with its license. Demo brand: the project's invented Aurora identity.




