About this animation
A reading welcome screen with a flowing sky and spring-driven book fan, with configurable illustration, books, colors, and fonts.
How it works
- A local illustration fills the central scene. A small Skia runtime shader displaces its upper region with two slow sine waves while leaving the reader steady.
- Forty deterministic stars fade in and twinkle independently. Two short meteors cross each nine-second ambient cycle.
- A continuous
fanvalue drives every cover's position, rotation, depth, and scale. Tapping a side cover or dragging selects it with an interruptible spring. - Each authentication letter uses its real Geist 700 advance, measured with Skia, along the arc length of the panel's concave quadratic edge. A trimmed Skia stroke travels under the selected action before its callback fires.
- The host can replay the entrance, focus a book, or highlight an action through the imperative handle. No React state updates run per frame.
Install
npx expo install @expo-google-fonts/geist @shopify/react-native-skia expo-image expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsCopy this folder without demo/ into your project. Wrap your app in GestureHandlerRootView. Load the font families passed to the component. On web, initialize CanvasKit before importing the component, following the Skia web setup for your application.
Usage
import { StorybookWelcome } from './storybook-welcome';
<StorybookWelcome
illustration={require('./assets/reader.jpg')}
books={[
{ id: 'ember', title: 'The Tiny Ember', cover: require('./assets/ember.jpg') },
{ id: 'pines', title: 'Girls of the Pines', cover: require('./assets/pines.jpg') },
{ id: 'court', title: 'The Moonlit Court', cover: require('./assets/court.jpg') },
]}
bottomInset={insets.bottom}
topInset={insets.top}
onSignUp={() => router.push('/sign-up')}
onOpenBook={(id) => openBook(id)}
/>Use skyMode="still" for a static scene or skyMode="sway" for a lower-cost ambient alternative. Customize all UI colors with colors, and all copy with labels:
<StorybookWelcome
illustration={reader}
books={books}
skyMode="sway"
starCount={24}
fontFamily="Geist_300Light"
titleFontFamily="Geist_700Bold"
serifFontFamily="InstrumentSerif_400Regular"
italicFontFamily="InstrumentSerif_400Regular_Italic"
colors={{ edge: '#C4BAFF', glow: '#FFFFFF' }}
labels={{ title: 'Evening Tales', signUp: 'Join Us' }}
/>Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
illustration | ImageSource | Required | Local reader and sky artwork |
books | StorybookBook[] | Required | IDs, titles, covers, optional title colors |
labels | Partial<StorybookLabels> | English welcome copy | All visible and accessibility copy; openBook supports {title} |
colors | Partial<StorybookColors> | Indigo and lavender | Every UI surface, ink, star, shadow, and highlight |
colorScheme | light / dark / auto | dark | Lavender surface palette; does not recolor artwork |
skyMode | flow / sway / still | flow on native and web | Ambient rendering cost and appearance |
starCount | number | 40 | Additional stars, clamped to 0–80 |
fontFamily | string | Geist_300Light | Welcome text |
titleFontFamily | string | Geist_700Bold | Heading, section, curved actions |
serifFontFamily | string | InstrumentSerif_400Regular | Cover titles |
italicFontFamily | string | InstrumentSerif_400Regular_Italic | Tagline |
monoFontFamily | string | — | Reserved for host API consistency |
bottomInset / topInset | number | 0 | Host-owned safe area, in points |
motion | auto / reduced / full | auto | OS motion preference or explicit override |
onSignIn / onSignUp | () => void | — | Called after 650 ms, or 220 ms in reduced motion |
onOpenBook | (id: string) => void | — | Tap on the centered cover |
onMenu | () => void | — | Menu action |
onInteraction | () => void | — | Stop a host autoplay on user input |
autoplay | boolean | false | Host demo flag; sequencing lives in the demo |
style / testID / accessibilityLabel | Common props | storybook-welcome ID | Container and automation |
The ref exposes focusBook(index), highlight('signIn' | 'signUp'), and replayIntro(). Highlight invokes the same callback as tapping. A new highlight or replay cancels the preceding pending callback.
Compatibility
Designed for Expo SDK 58, React Native 0.88, Reanimated 4.7, Gesture Handler 3.3, Worklets 0.13, and Skia 2.14. Uses modules bundled in Expo Go on iOS and Android. Web uses the same gestures with CanvasKit and defaults to localized hair flow. Use sway for a lower-cost alternative.
The component measures its container. Tablet and landscape layouts center a phone-width stage; compact layouts retain 48-point action regions. The host supplies safe area insets. Use source images at least three times their displayed dimensions for crisp high-density rendering. The supplied 768-pixel demo illustration is an art-direction sample; production artwork should be at least 1290 pixels wide for a 430-point screen.
Accessibility
Every action has a button role and configurable label. Focused books expose their selected state. Side-cover taps provide an alternative to dragging. The semantic authentication state is exposed as auth none, auth signIn, or auth signUp. Reduced motion freezes the sky and stars, removes meteors and parallax, and replaces large transitions with a 220 ms entrance fade. Curved action containers expose one readable label, while individual decorative letters remain grouped out of the accessibility tree.
Why this one
The hair and the sky read as one illustrated world. A responsive book fan and a light that follows the curved panel edge turn a static welcome composition into a tactile introduction without adding navigation or authentication dependencies.




