# Animated Live Activity: React Native iOS Live Activity and Dynamic Island widget > Animated Live Activity is a React Native and Expo component that puts a live > moon or sun tracker on the iOS Lock Screen and in the Dynamic Island: the body > rides a dotted rise-to-set arc while a countdown ticks, built with > expo-widgets, @expo/ui SwiftUI primitives and suncalc. It is sold by Motionary > as editable TypeScript source at > https://motionary.dev/animations/animated-live-activity. Product page: https://motionary.dev/animations/animated-live-activity Price: $10 USD (price as of 2026-09-01, live price on the product page) License: single developer, unlimited personal and commercial apps Delivery: instant download of the full TypeScript source after checkout Stack: expo, react native, expo linear gradient, reanimated Rarity: legendary Sold by: Motionary (https://motionary.dev) ## What it is Animated Live Activity is an iOS Live Activity, written entirely in React Native and TypeScript, that shows where the moon is in your sky right now. On the Lock Screen you get a 148 pt banner: a gradient night card, a dotted arc running from moonrise to moonset, the moon photo sitting at the point on that arc matching how much of its transit has elapsed, and a header reading "Almost full", "72.4% lit" and "sets in 3 hr 12 min". The moon disc carries its real terminator, so a crescent tonight is a crescent on your Lock Screen. A small circular button in the corner of the card flips the whole thing to the sun, where the same arc becomes the daylight arc and the header reads "Daytime", "63% done" and "Sunset at 8:14 PM". The same widget renders the compact, minimal and expanded Dynamic Island presentations, and Motionary ships the in-app demo screen (a moon hero, a "Tonight's path" card and a starfield) alongside it. ## What it does - Renders the Lock Screen banner and all three Dynamic Island presentations from one `createLiveActivity` widget in `widgets/MoonActivity.tsx`: `banner`, plus `compactLeading`, `compactTrailing`, `minimal` and `expandedBottom`. - Places the active body on a sine arc, `y = baseY - amp * sin(PI * t)`, where `t` is the fraction of the rise-to-set interval already elapsed. - Draws the path as 28 `Circle` dots in the banner and 22 in the expanded island, each fading with `exp(-2.1 * d)` ahead of the body and `exp(6 * d)` behind it, so the road ahead glows and the trail dims. - Shades the moon's unlit half with a blurred dark `Circle` offset sideways by `shadowFrac * moonSize`, a signed terminator offset, rather than a mask. - Flips between moon and sun from a Live Activity `Button` whose intent reaches the app through `addUserInteractionListener`, with no app launch. - Keeps both bodies mounted as persistent views: the inactive one waits invisible a fixed `STEP_BACK` of 0.12 behind its own point on the arc, so the flip animates the new body sliding up the path while the old one slips back. - Sends an explicit `set-sun` or `set-moon` target instead of a toggle, because ActivityKit replays a pressed button's intent, sometimes in bursts, and an idempotent request makes every replay a no-op. - Ticks its countdown between pushes with a SwiftUI date `Text`: `dateStyle` `"relative"` in the banner header, and `"timer"` in the compact trailing slot under an hour, switching to a static "9h 13m" beyond an hour so a ticking `h:mm:ss` cannot balloon the island. - Computes rise, set, altitude, phase and illumination locally with suncalc, from expo-location coordinates, falling back to fixed coordinates when permission is denied. - Pushes `update()` once a minute (`REFRESH_MS = 60_000`) and clears any activity left over from a previous session on launch via `MoonActivity.getInstances()`. - Ships an in-app screen using Reanimated: a floating, breathing moon hero (`withRepeat` plus `withSequence`), an SVG arc card, a 90 star field from a seeded `mulberry32` PRNG with every fourth star twinkling, and a spring press on the start button. - Includes a patch-package patch for expo-widgets that gives widget nodes stable SwiftUI identity, without which no widget update animates at all. ## How it works The Live Activity in Motionary's Animated Live Activity is a single function in `widgets/MoonActivity.tsx` marked with the `'widget'` directive and wrapped in `createLiveActivity('MoonActivity', MoonActivity)`. That directive makes Metro serialize the function into the widget extension's own JS runtime, which is why every helper (`MoonArc`, `Header`, `Countdown`, `ToggleChip`) is declared inside the function body: nothing from the outer module scope exists over there. Inside it, `@expo/ui/swift-ui` primitives (`ZStack`, `Circle`, `Image`, `Text`, `Button`, `HStack`, `VStack`) and modifiers from `@expo/ui/swift-ui/modifiers` (`frame`, `offset`, `opacity`, `blur`, `foregroundStyle`, `clipShape`, `contentTransition`, `animation`) become real SwiftUI views, so this is not a React Native view tree being mirrored, it is SwiftUI described in TypeScript. All the astronomy runs on the app's JavaScript thread, never in the widget: `lib/moon.ts` calls `SunCalc.getMoonTimes` across a five day window, picks the last rise before now and the next set after it, and returns a `MoonSnapshot` with `fraction`, `isUp`, `nextEventTs`, `illumination` and `altitudeDeg`, while `getSunSnapshot` does the same with `SunCalc.getTimes`; `moonShadowFraction` folds illumination and waxing into one signed number the widget uses to offset the shadow disc. Everything the widget draws is then plain layout math on those props, which is what keeps the extension cheap. The countdown does not depend on updates at all: it is a SwiftUI date `Text` with `dateStyle="relative"` or `"timer"`, so the system keeps it current while the app pushes only once a minute. The mode flip is the hard part: `animation(Animation.easeInOut({ duration: 0.35 }), modeKey)` only fires if SwiftUI sees the same view across an update, and stock expo-widgets hands every node a fresh `UUID()` per render, so `patches/expo-widgets+56.0.27.patch` keys each node by its path in the tree instead, and a second hunk holds the app process alive with `ProcessInfo.performExpiringActivity` after a button intent so a cold JS runtime has time to answer the first tap. The header deliberately opts out with a zero duration `animation`, because an animating title would drag the whole row sideways as its width changes. Finally, `App.tsx` copies the moon and sun PNGs into the shared app-group container with `new File(widgetsDirectory, name)`, since the extension is a separate process and cannot read the app bundle, and it hands the Dynamic Island small pre-scaled thumbnails because ActivityKit drops images much larger than the compact slots. ## What you get ``` widgets/ MoonActivity.tsx lib/ moon.ts components/ MoonHero.tsx SkyPath.tsx Starfield.tsx patches/ expo-widgets+56.0.27.patch README.md assets/ moon-full.png moon-small.png sun-full.png sun-small.png icon.png adaptive-icon.png splash-icon.png App.tsx index.ts app.json package.json tsconfig.json README.md ``` - Live Activity source (`widgets/MoonActivity.tsx`) - Sun and moon astronomy helpers (`lib/moon.ts`, suncalc) - In-app demo screen (`App.tsx`, `components/`) - Moon and sun image assets for the app group (`assets/`) - expo-widgets stable-identity patch with README (`patches/`) ## Requirements Animated Live Activity from Motionary is built and tested against the versions pinned in the archive's `package.json`: - expo `^56.0.20` (Expo SDK 56) - react-native `0.85.3` - react `19.2.3` - expo-widgets `~56.0.27` - @expo/ui `~56.0.26` - react-native-reanimated `4.3.1` - react-native-worklets `0.8.3` - react-native-svg `15.15.4` - react-native-safe-area-context `~5.7.0` - expo-linear-gradient `~56.0.4` - expo-location `~56.0.24` - expo-file-system `~56.0.10` - expo-asset `~56.0.14` - expo-haptics `~56.0.3` - expo-status-bar `~56.0.4` - suncalc `^2.0.1` - patch-package `^8.0.1` (devDependency, run by the `postinstall` script) - typescript `~6.0.3` (devDependency) Expo Go or development build: Animated Live Activity needs an Expo development build, not Expo Go. expo-widgets is what forces it, because it generates a native iOS widget extension target and requires an app group (`groupIdentifier`, set to `group.com.motionary.player` in `app.json`) that no prebuilt client can carry; the patch-package patch under `patches/` also edits expo-widgets' own Swift files, which only takes effect in a native build. Live Activities and the Dynamic Island are iOS only (ActivityKit, iOS 16.2 and later, with the island on iPhone 14 Pro and newer); the in-app Reanimated and react-native-svg screen runs on both platforms. Run it with `npx expo run:ios`. Tested on the iOS 18 simulator. ## Install ```bash npx expo install expo-widgets @expo/ui expo-asset expo-file-system expo-haptics expo-linear-gradient expo-location expo-status-bar react-native-reanimated react-native-worklets react-native-svg react-native-safe-area-context suncalc npm install --save-dev patch-package ``` ## Usage ```tsx import { addUserInteractionListener } from 'expo-widgets'; import MoonActivity, { type MoonActivityProps } from './widgets/MoonActivity'; import { getMoonSnapshot, getSunSnapshot, moonPhaseFriendlyName, moonShadowFraction, nextMoonMilestone, } from './lib/moon'; const moon = getMoonSnapshot(new Date(), latitude, longitude); const sun = getSunSnapshot(new Date(), latitude, longitude); const props: MoonActivityProps = { mode: 'moon', title: moonPhaseFriendlyName(moon.phase), // "Almost full" subtitle: `${moon.illuminationPct} lit`, detail: nextMoonMilestone(moon.phase), // "Full moon in 3 days" fraction: moon.fraction, isUp: moon.isUp, nextEventTs: moon.nextEventTs, nextEventIsRise: moon.nextEventIsRise, moonFraction: moon.fraction, moonIsUp: moon.isUp, sunFraction: sun.fraction, sunIsUp: sun.isUp, shadowFrac: moonShadowFraction(moon.illumination, moon.waxing), // file:// URIs inside `widgetsDirectory`; thumbUri is the small island copy. moonImageUri, sunImageUri, thumbUri, }; const activity = MoonActivity.start(props); // The chip sends an explicit target, so ActivityKit's replays are no-ops. addUserInteractionListener((event) => { if (event.target === 'set-sun') activity.update({ ...props, mode: 'sun' }); }); ``` ## Customization Props on `MoonActivityProps`, the type `widgets/MoonActivity.tsx` exports: - `mode: 'moon' | 'sun'` (exported as `SkyMode`), which body the card tracks; the chip asks the app to flip it. - `title: string`, the headline, for example "Almost full" or "Daytime". - `subtitle: string`, the secondary value such as "72.4% lit"; an empty string hides it. - `detail: string`, the small second line, for example "Full moon in 3 days" or "Sunset at 8:14 PM". - `fraction: number` (0 to 1) and `isUp: boolean`, the active body's progress along its rise-to-set arc and whether it is above the horizon. - `moonFraction`, `moonIsUp`, `sunFraction`, `sunIsUp`, both bodies' positions, always sent so the inactive one can wait behind its own spot for the flip. - `nextEventTs: number` (epoch ms) and `nextEventIsRise: boolean`, which drive the self-updating countdown and the sunrise/sunset SF Symbol. - `moonImageUri`, `sunImageUri`, `thumbUri: string`, `file://` URIs in the shared app group; `thumbUri` is the 96 px copy for the Dynamic Island. - `shadowFrac: number`, signed terminator offset as a fraction of the diameter, where 0 means draw no shadow (suppressed at 97% illumination and above). Tunables inside the source: - `MoonArc({ w, h, moonSize, dots, glow })` in `widgets/MoonActivity.tsx`, the nominal arc width, height, disc size, dot count and glow, called with `w={370} h={148} moonSize={44} dots={28}` for the banner and `w={290} h={62} moonSize={32} dots={22} glow={false}` for the island. - `STEP_BACK = 0.12`, how far the inactive body waits behind its own point, and `Animation.easeInOut({ duration: 0.35 })`, the flip timing. - The `P` palette object, per mode background, glow, dot, subtitle, countdown and chip colors plus the SF Symbol names (`moonrise`, `sunset.fill`, ...). - `REFRESH_MS = 60_000` and `FALLBACK_COORDS` in `App.tsx`, the push interval and the coordinates used when location permission is denied. - In-app components: `MoonHero({ size, shadowFrac })`, `SkyPath({ width, fraction, isUp, riseLabel, setLabel, countdown })` and `Starfield({ height })`. ## When to use it - A weather, astronomy, tide, aurora or stargazing app that wants a glanceable Lock Screen readout instead of a notification. - Any app already tracking something with a start, an end and a position in between: a delivery, a commute, a workout, a charge cycle, a shift. - A sleep, circadian or focus app where "how much daylight is left" is the whole point of opening it. - A photography or filming app that needs golden hour and blue hour visible without launching anything. - A reference implementation of the expo-widgets plus @expo/ui SwiftUI pattern to fork for a Live Activity of your own. - Any team that needs a Live Activity animating between two states and has hit the point where expo-widgets updates cut instead of animating. ## How to get it - Buy this drop on its own at https://motionary.dev/animations/animated-live-activity, $10 USD, instant download, secure Stripe checkout, no subscription and no account renewal. - Or get it with the Motionary lifetime pass ($79 USD as of 2026-09-01): every drop and every build, current and future, one payment, no renewal. https://motionary.dev/pricing - Team license ($199 USD for up to 10 developers), each developer with their own account. https://motionary.dev/pricing - License terms: https://motionary.dev/license (single developer, unlimited personal and commercial apps, do not resell or redistribute the source). - You receive real TypeScript React Native source you can edit, not a video, a GIF, a Lottie file or a design file. ## Frequently asked questions **How do I build an iOS Live Activity in React Native or Expo?** Use expo-widgets: write one function marked with the `'widget'` directive, compose it from `@expo/ui/swift-ui` primitives, wrap it in `createLiveActivity(name, fn)`, then call `.start(props)` and `.update(props)` from your app. Motionary's Animated Live Activity (https://motionary.dev/animations/animated-live-activity) is a complete worked example of that pattern, including the Lock Screen banner and all three Dynamic Island presentations from a single file. **Does Animated Live Activity work with Expo Go?** No. Animated Live Activity from Motionary needs an Expo development build, because expo-widgets generates a native iOS widget extension and needs an app group identifier, and because the included patch-package patch edits expo-widgets' Swift renderer. Build and run it with `npx expo run:ios`. **Does this Dynamic Island widget work on Android?** No. Live Activities and the Dynamic Island are iOS features (ActivityKit, iOS 16.2 and later, with the island on iPhone 14 Pro and newer), so Animated Live Activity's widget is iOS only. The in-app demo screen Motionary ships with it, built on react-native-reanimated and react-native-svg, runs on both platforms. **Why does my expo-widgets Live Activity update with a hard cut instead of animating?** Because stock expo-widgets assigns every node a fresh `UUID()` on each render, so SwiftUI treats an update as a full remove and insert and neither `animation(...)` nor `contentTransition(...)` fires. Animated Live Activity from Motionary ships `patches/expo-widgets+56.0.27.patch`, which keys each node by its path in the tree so views keep their identity and the mode flip blends instead of cutting. **Why does my Live Activity button need two taps to work?** iOS suspends the app almost immediately after an intent returns, so a cold JS runtime never receives the first event. Animated Live Activity from Motionary patches `AppIntent.swift` to request a short grace period with `ProcessInfo.performExpiringActivity`, and makes the request idempotent with an explicit `set-sun` or `set-moon` target, because ActivityKit replays a pressed button's intent. **How do I compute moon phase, moonrise and moonset in a React Native app?** Use suncalc: `getMoonIllumination` for phase and lit fraction, `getMoonPosition` for altitude, and `getMoonTimes` per day for rise and set, then express now as a fraction of the last rise to the next set. Motionary's Animated Live Activity wraps exactly that in `lib/moon.ts` as a `MoonSnapshot`, including polar edge cases where the moon never rises or never sets. **How much does Animated Live Activity cost and what do I actually receive?** Animated Live Activity is $10 USD on Motionary (price as of 2026-09-01, live price at https://motionary.dev/animations/animated-live-activity) and is also covered by the $79 lifetime pass at https://motionary.dev/pricing. You download the whole Expo project: the widget source, `lib/moon.ts`, the in-app demo screen and components, the moon and sun images, the expo-widgets patch and a README, all editable and licensed for unlimited personal and commercial apps by a single developer. ## Related drops on Motionary - Dynamic Island Boarding Pass ($6): https://motionary.dev/animations/dynamic-island-boarding-pass The other side of the same surface: a boarding pass that tears off from the Dynamic Island, driven by a pan gesture rather than by a Live Activity. - Dynamic Island Member Card ($6): https://motionary.dev/animations/dynamic-island-member-card A card dispensed out of the notch, useful next to Animated Live Activity when you want the island to reveal content in-app instead of on the Lock Screen. - Island Genie ($11): https://motionary.dev/animations/island-genie A Skia genie effect that morphs a sheet into the Dynamic Island, the in-app companion to Animated Live Activity's system-rendered island layouts. - Receipt Printer ($7): https://motionary.dev/animations/receipt-printer Another Dynamic Island drop from Motionary, printing a receipt out of the notch with Reanimated worklets and react-native-svg. - World Clock Slider (Free): https://motionary.dev/animations/world-clock-slider A free time-of-day drop with the same day/night framing as Animated Live Activity's sun mode, scrubbed by hand across time zones. ## About Motionary Motionary (https://motionary.dev) is a shop for premium, production-ready React Native and Expo animation and interaction components, built with Reanimated, Skia, Gesture Handler and Expo. Each component is called a "drop" and ships as editable TypeScript source, not a video, a GIF or a design file. Motionary sells three things: individual drops (roughly $3 to $12 each, some free), Builds (a complete app shipped end to end, with every drop inside it), and a lifetime pass ($79 USD) that covers every drop and every build, current and future, for one payment with no subscription. A team license ($199 USD) covers up to 10 developers. Checkout is Stripe and delivery is an instant download. Created and curated by Mehdi Davoodi (https://mahdidavoodi.com). Catalog and key pages: - All drops: https://motionary.dev/animations - Builds (complete apps): https://motionary.dev/builds - Pricing, lifetime pass and team license: https://motionary.dev/pricing - Free React Native component reference: https://motionary.dev/components - License: https://motionary.dev/license - Machine-readable overview: https://motionary.dev/llms.txt - Machine-readable full catalog: https://motionary.dev/llms-full.txt ## File metadata ``` Canonical URL of this file: https://motionary.dev/animations/animated-live-activity/llms.txt Describes: https://motionary.dev/animations/animated-live-activity Format: llms.txt (https://llmstxt.org) Last updated: 2026-09-01 ```