About this animation
A scroll-driven lunar sleep program with changing moon phases, with configurable chapters, program title, fonts, and accent color.
Why this one
Instead of swapping phase icons, a single spherical moon continuously changes its illuminated face. Earthshine keeps the dark face visible, and the lake becomes a path of light at full moon. The scroll acts as both a story and a calendar.
How it works
onLayoutmeasures the container. One progress value,p = scrollY / height, drives all five chapters and the lunar calendar.- A Skia runtime shader projects a sphere against a changing light direction. Its terminator is an ellipse with minor axis
cos(angle) × radius, softened across approximately 6 points. Fractal noise and 18 soft crater depressions supply local texture. - Pose interpolation rests for the first and last 20% of each chapter interval. The moon follows a shallow arc and rotates 8 degrees across the cycle.
- The sky, 160 deterministic stars, full-moon halo, segmented lake reflection, and a shooting star respond to lunar illumination. Reading veils are part of the shader.
- Headings have separate entrance and exit windows with a gap between adjacent chapters. The night counter rolls only during the last quarter of each integer change; tens roll only on carry.
- Native scrolling snaps to chapters. Web scrolling remains continuous. The final action belongs to the scroll content. Demo autoplay uses the ref to move the actual scroll, then fades to black to restart.
Install
npx expo install @shopify/react-native-skia react-native-reanimated react-native-workletsCopy this folder into your project; demo/ is optional. Load CanvasKit before mounting on web, following Skia's web setup. Expo configures the Reanimated Babel plugin automatically.
Usage
import { MoonPhaseScroll } from './moon-phase-scroll';
export function SleepProgram() {
return <MoonPhaseScroll onStart={() => openProgram()} />;
}Supply your loaded fonts and use the ref for external navigation:
import { useRef } from 'react';
import { MoonPhaseScroll, type MoonPhaseScrollRef } from './moon-phase-scroll';
export function LunarJourney() {
const moon = useRef<MoonPhaseScrollRef>(null);
return (
<MoonPhaseScroll
ref={moon}
fontFamily="YourSans"
titleFontFamily="YourSerif"
italicFontFamily="YourSerifItalic"
monoFontFamily="YourMono"
accentColor="#C4B5FD"
moonRatio={0.62}
haloIntensity={1.2}
onStart={() => openProgram()}
onChapterChange={(chapter) => trackChapter(chapter)}
/>
);
}The examples use host-provided openProgram and trackChapter callbacks. scrollToChapter(chapter, animated = true) accepts fractional chapters from 0 through 4. reset() fades to black, returns to chapter 0, and fades back in. Navigation can always be interrupted by touch or wheel input.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
chapters | Tuple of five MoonChapter objects | Lunar sleep chapters | Titles, subtitles, eyebrows, descriptions, and sessions. |
header | ReactNode | — | Host brand content above the program title. |
programTitle | string | Sleep with the moon | Small program heading. |
actionLabel | string | Start the 29-night program | Final button label and accessibility label. |
onStart | () => void | — | Final action callback. |
onChapterChange | (chapter: number) => void | — | Nearest chapter changed; called on the JS thread. |
onInteraction | () => void | — | User interrupts external playback. |
fontFamily | string | System | Interface text. |
titleFontFamily | string | System | First heading line. |
italicFontFamily | string | System | Editorial second line. |
monoFontFamily | string | System | Counters and eyebrows. |
accentColor | string | #C4B5FD | Valid Skia color for text, halo, reflection, and action. |
moonRatio | number | 0.62 | Moon diameter divided by container width; clamped to 0.3–0.75. |
haloIntensity | number | 1 | Full-moon halo multiplier; clamped to 0–2. |
transitionDuration | number | 2200 | Imperative scroll animation duration in milliseconds. |
snapToChapters | boolean | true | Chapter snapping on native platforms only. |
style | StyleProp<ViewStyle> | — | Root container style. |
testID | string | — | Root test identifier. |
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Skia 2.14.0. Dependencies are included in Expo Go. iOS, Android, and web are compilation targets. No network assets or additional native dependencies are required.
Accessibility
Chapter navigation and the first-screen scroll hint are labeled buttons with targets of at least 44 points. Each scroll chapter exposes a header block containing its complete copy. Inactive visual headings are hidden from screen readers. The final action has a button role and uses actionLabel as its label.
Reduced motion freezes the moon's pose, rotation, ripples, and hint, removes the meteor, and replaces continuous phase changes with 200 ms scene fades. Chapter text fades in over 200 ms without moving or overlapping another heading. Chapter buttons provide a tap alternative to scrolling.
Icons: Lucide (ISC). The demo uses the Aurora brand from the project kit; production content and font names are supplied by the host.




