@parrotnavy/react-native-dynamic-splash
v1.0.0
Published
Native-first dynamic splash screen with remote config, animated GIF/APNG support, and background asset caching for React Native
Maintainers
Readme
💐 React Native Dynamic Splash
If this library saved you time, consider buying me a coffee. It really helps.
A native-first dynamic splash layer for React Native that intentionally occupies the moment between OS launch and app readiness.
Overview
react-native-dynamic-splash is not a generic full-screen banner or in-app advertisement component.
[!IMPORTANT] Its purpose is to provide a dynamic, full-screen transition layer that appears immediately after the OS-level splash screen and before any app UI is rendered.
This library intentionally targets the narrow but critical time window between:
- The operating system’s mandatory static splash screen, and
- The moment when the React Native application becomes interactive.
What This Is
- A dynamic splash / initial transition screen
- Displayed before app content, not on top of it
- Controlled natively (iOS / Android), independent of JS readiness
- Capable of acting as:
- A loading screen
- A branded entry experience
- A marketing / campaign surface
From a UX perspective, users perceive this as part of the app startup sequence — not as an interruptive ad.
What This Is NOT
- ❌ A modal or screen rendered inside React Navigation
- ❌ A post-launch in-app advertisement
- ❌ A generic full-screen banner component
- ❌ A replacement for OS-level LaunchScreen assets
Execution Model
The startup timeline is intentionally structured as:
sequenceDiagram
participant OS as OS Splash
participant DS as Dynamic Splash
participant APP as App UI
OS->>DS: 1. OS splash ends
DS->>DS: 2. Show dynamic splash immediately
DS->>DS: 3. App is loading in background
DS->>APP: 4. App signals ready
DS->>DS: 5. Hide splash (fade / timing)
DS->>APP: 6. App UI becomes visible- OS Static Splash
- Android launch theme
- iOS
LaunchScreen.storyboard
- Dynamic Splash (this library)
- Shown immediately after OS splash
- Uses cached, pre-validated assets
- Can be animated (GIF / APNG)
- Enforced timing via native code
- App UI
- Hidden until JS signals readiness
This makes the dynamic splash a first-class startup layer, not an application screen.
Architectural Principles
- Native-first display
- Overlay window is fully managed by native modules
- JS for data, not rendering
- JavaScript only fetches configs, downloads assets, and persists metadata
- Next-launch determinism
- Assets are prepared ahead of time and only shown on the next app launch
- Fail-closed behavior
- Any validation or IO failure results in no splash being shown
- No coupling to navigation or app state
- The splash lifecycle is independent from React rendering
Typical Use Cases
- Startup branding without rebuilding the app
- Campaign or promotion display at app launch
- Masking cold-start latency with intentional visuals
- Gradual replacement of static splash experiences
In short:
This library exists to intentionally design the moment between “app launched” and “app ready”.
Installation
Bare React Native
npm install @parrotnavy/react-native-dynamic-splash react-native-fs && npx pod-install
# or
yarn add @parrotnavy/react-native-dynamic-splash react-native-fs && npx pod-installExpo (Managed Workflow)
npx expo install @parrotnavy/react-native-dynamic-splash react-native-fsAdd the plugin to your app.json (or app.config.js):
{
"expo": {
"plugins": ["@parrotnavy/react-native-dynamic-splash"]
}
}[!IMPORTANT] This library uses native code via a Config Plugin. It requires a Development Build and cannot be tested in the standard Expo Go app.
npx expo run:ios # or run:androidRequirements
| Platform | Minimum Version | |----------|-----------------| | iOS | 12.0+ | | Android | API 21+ (animated images require API 28+) | | Node.js | 20.0.0+ | | React Native | 0.70+ |
Native Integration
Show the READY splash content natively (based on stored metadata), then hide it once JS is ready. This does not touch or reuse LaunchScreen assets.
iOS (Swift - AppDelegate.swift)
import react_native_dynamic_splash // <-- Add like this
@main
class AppDelegate: RCTAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
DynamicSplashNative.show() // <-- Add like this
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}iOS (Objective-C - AppDelegate.m)
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import "RNDynamicSplash.h" // <-- Add like this
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"YourAppName";
self.initialProps = @{};
[RNDynamicSplash show]; // <-- Add like this
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@endAndroid (MainActivity)
import com.reactnativedynamicsplash.DynamicSplashNativeModule; // <-- Add like this
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DynamicSplashNativeModule.show(this); // <-- Add like this
}Optional storage key override (use only if you changed storageKey in JS):
Swift:
// DynamicSplashNative.setStorageKey("MY_CUSTOM_KEY") // Uncomment if using custom storageKey
DynamicSplashNative.show()Objective-C:
// [RNDynamicSplash setStorageKey:@"MY_CUSTOM_KEY"]; // Uncomment if using custom storageKey
[RNDynamicSplash show];Android:
// DynamicSplashNativeModule.setStorageKey("MY_CUSTOM_KEY"); // Uncomment if using custom storageKey
DynamicSplashNativeModule.show(this);Then, once JS is ready, you can hide the splash:
import { DynamicSplash } from "@parrotnavy/react-native-dynamic-splash";
// Hide the splash (respects minDurationMs and fade settings)
DynamicSplash.hide();
// Check if splash is currently visible
const isVisible = await DynamicSplash.isVisible();Quick Start
Step 1: Create a splash config file
// splashConfig.ts
import type { InitOptions } from "@parrotnavy/react-native-dynamic-splash";
export const splashConfig: InitOptions = {
// Fetch config from your backend or CDN
configProvider: async () => {
const response = await fetch("https://your-api.com/splash.json");
return response.json();
},
// Optional: Debug logging during development
logger: (msg, ...args) => console.log("[Splash]", msg, ...args),
// Timing constraints
minDurationMs: 2000,
maxDurationMs: 3000,
// Animations
animation: {
fade: { enabled: true, durationMs: 200 },
scale: { startScale: 1.0, endScale: 1.02, durationMs: 2000, easing: "linear" },
},
};Step 2: Initialize in your App component
// App.tsx
import { useEffect, useMemo } from "react";
import { createDynamicSplash, DynamicSplash } from "@parrotnavy/react-native-dynamic-splash";
import { splashConfig } from "./splashConfig";
function App() {
// Create manager instance (singleton)
const manager = useMemo(() => createDynamicSplash(splashConfig), []);
useEffect(() => {
// Start background update to fetch config and cache images
manager.mount();
}, [manager]);
useEffect(() => {
// Hide splash when your app is ready
// (e.g., after data loading, auth check, etc.)
const timer = setTimeout(() => {
DynamicSplash.hide();
}, 3000);
return () => clearTimeout(timer);
}, []);
return (
// Your app content
);
}Step 3: Host your splash config JSON
[
{
"imageName": "welcome-offer-2026q1",
"alt": "Welcome offer: 20% off your first order",
"startAt": "2026-01-01T00:00:00Z",
"endAt": "2026-03-31T23:59:59Z",
"imageUrl": "https://cdn.example.com/splash/welcome.png",
"configVersion": "2026.01.0",
"backgroundColor": "#0B1220",
"weight": 2
},
{
"imageName": "spring-sale-2026",
"alt": "Spring sale: Up to 50% off",
"startAt": "2026-03-01T00:00:00Z",
"endAt": "2026-04-30T23:59:59Z",
"imageUrl": "https://cdn.example.com/splash/spring-sale.gif",
"configVersion": "2026.03.0",
"backgroundColor": "#101828",
"weight": 3
}
][!IMPORTANT] Images are downloaded and cached on the device. The splash is displayed on the next app launch after a successful download.
Configuration
InitOptions
type InitOptions = {
/** Provides splash config objects from any source (required). */
configProvider: () => Promise<SplashConfig | SplashConfig[]>;
/** Minimum time to keep splash visible (ms). Applied by native modules. Default: no minimum. */
minDurationMs?: number;
/** Maximum time to keep splash visible (ms). Native auto-hides after this duration. Default: no maximum. */
maxDurationMs?: number;
/**
* Animation settings for show/hide.
* - fade: hide-only fade-out.
* - scale: applied on show.
*/
animation?: {
fade?: {
/** Enable fade out animation when hiding (default: true). */
enabled?: boolean;
/** Fade out animation duration in milliseconds (default: 200). */
durationMs?: number;
};
scale?: {
/** Start scale factor (e.g., 1.0). */
startScale: number;
/** End scale factor (e.g., 1.2). */
endScale: number;
/** Animation duration in milliseconds. */
durationMs: number;
/** Animation easing (default: easeInOut). */
easing?: "linear" | "easeIn" | "easeOut" | "easeInOut";
};
};
/** @deprecated Use animation.fade.enabled instead. */
enableFade?: boolean;
/** @deprecated Use animation.fade.durationMs instead. */
fadeDurationMs?: number;
/** Storage key for persisted metadata (default: "DYNAMIC_SPLASH_META_V1"). */
storageKey?: string;
/**
* Storage location for cached images.
* - "document": Persists across app restarts; better for long-lived assets.
* - "cache": Uses OS-managed cache; may be evicted by the system.
* Default: "document".
*/
fileLocation?: "document" | "cache";
/** Optional config processed immediately on mount (pre-seeding, default: none). */
initialConfig?: SplashConfig | SplashConfig[];
/** Optional logging hook (default: none). */
logger?: (msg: string, ...args: any[]) => void;
/** Wait timeout for storage readiness before skipping dynamic splash (ms, default: 0). */
storageReadyTimeoutMs?: number;
/** If true, show the splash immediately after a successful update in the same session (default: false). */
showOnUpdate?: boolean;
};JSON Contract
Required properties are minimal and focused on determinism and caching.
type SplashConfig = {
/** Image identifier used as the cache key and file name. */
imageName: string;
/** Accessibility description for the image. */
alt: string;
/** Start time (ISO-8601). Splash is eligible only if now >= startAt. */
startAt: string;
/** End time (ISO-8601). Splash is eligible only if now <= endAt. */
endAt: string;
/** Download URL (http/https). Used to fetch the remote asset. */
imageUrl: string;
/** Per-image config version. If changed, cached image is invalidated. */
configVersion: string;
/** Background color shown behind the image if it does not fill the screen (default: none). */
backgroundColor?: string;
/** Relative weight (>= 0). Higher values increase selection probability (default: 1). */
weight?: number;
};Example (array is supported; weight is a ratio):
[
{
"imageName": "promo-pink",
"alt": "Pink Hug",
"startAt": "2023-01-01T00:00:00Z",
"endAt": "2026-12-31T23:59:59Z",
"imageUrl": "https://example.com/promo-pink.png",
"configVersion": "promo-pink-v1",
"backgroundColor": "#FFC0CB",
"weight": 5
},
{
"imageName": "promo-blue",
"alt": "Blue Theme",
"startAt": "2023-01-01T00:00:00Z",
"endAt": "2026-12-31T23:59:59Z",
"imageUrl": "https://example.com/promo-blue.png",
"configVersion": "promo-blue-v3",
"backgroundColor": "#ADD8E6",
"weight": 3
}
]Config Provider
Provide config objects from any source (HTTP endpoint, Firebase Remote Config, feature flags, etc.):
// From HTTP endpoint
createDynamicSplash({
configProvider: async () => {
const response = await fetch('https://api.example.com/splash.json');
return response.json();
},
});
// From Firebase Remote Config
createDynamicSplash({
configProvider: async () => {
await remoteConfig().fetchAndActivate();
const config = remoteConfig().getString('splash_config');
return JSON.parse(config);
},
});
// Static config
createDynamicSplash({
configProvider: async () => ({
imageName: "promo-pink",
alt: "Pink Hug",
startAt: "2023-01-01T00:00:00Z",
endAt: "2026-12-31T23:59:59Z",
imageUrl: "https://example.com/promo-pink.png",
configVersion: "promo-pink-v1",
backgroundColor: "#FFC0CB",
weight: 5,
}),
});Update Rules (Practical)
- JSON is fetched on each launch (
mount()). - A cached asset is reused when:
status === "READY"imageNamematchesconfigVersionmatches- local file exists
- Any failure results in
ERRORand no display on next launch.
API
createDynamicSplash(options: InitOptions)
Creates a manager instance for background updates.
const manager = createDynamicSplash({
configProvider: async () => ({ /* config */ }),
minDurationMs: 2000,
maxDurationMs: 5000,
animation: {
fade: { enabled: true, durationMs: 500 },
},
});manager.mount()
Triggers background update to fetch and cache splash images. Call this once when your app starts.
useEffect(() => {
manager.mount();
}, [manager]);DynamicSplash.hide()
Requests native module to hide the splash overlay. Respects minDurationMs and applies fade animation if enabled.
DynamicSplash.hide();DynamicSplash.isVisible()
Returns current visibility state from native module.
const isVisible = await DynamicSplash.isVisible();
console.log('Splash visible:', isVisible);Animated Images
Supported Formats
- GIF: Fully supported on iOS (all versions) and Android (API 28+)
- APNG: Fully supported on iOS (all versions) and Android (API 28+)
- Static images: PNG, JPEG (all platforms)
Fallback Behavior
- Android < API 28: Animated images display as static (first frame only)
- iOS: All formats fully supported via
ImageIOframework
Limits
- Maximum frames: 200 frames per animated image (to prevent memory issues)
- Images exceeding this limit will be truncated to the first 200 frames
Usage
Simply provide a GIF or APNG URL in your config:
{
"imageName": "animated-promo",
"imageUrl": "https://example.com/promo.gif",
"configVersion": "v1"
}The native modules automatically detect and animate the image.
Fade Effects
Fade effects are configured globally in InitOptions (not per-splash-item):
createDynamicSplash({
configProvider: async () => ({ /* ... */ }),
animation: {
fade: {
enabled: true, // Enable fade out animation
durationMs: 500, // 500ms fade duration
},
},
});animation.fade.enabled: true- Smooth fade out when hidinganimation.fade.enabled: false- Instant hide (no animation)animation.fade.durationMs- Duration in milliseconds (default: 200)
Deprecated (still supported):
createDynamicSplash({
configProvider: async () => ({ /* ... */ }),
enableFade: true,
fadeDurationMs: 500,
});Fade settings are stored in metadata and applied by native modules on the next launch.
Scale Effects
Scale effects are configured globally in InitOptions:
createDynamicSplash({
configProvider: async () => ({ /* ... */ }),
animation: {
scale: {
startScale: 1.0,
endScale: 1.1,
durationMs: 1000,
easing: "easeInOut",
},
},
});scaleruns when the splash is shown.scale.easingcan be one oflinear,easeIn,easeOut,easeInOut.
Timing Control
Both minDurationMs and maxDurationMs are enforced by native modules:
createDynamicSplash({
configProvider: async () => ({ /* ... */ }),
minDurationMs: 2000, // Splash shows for at least 2 seconds
maxDurationMs: 5000, // Auto-hide after 5 seconds
});- minDurationMs: Native delays hide requests until minimum time has elapsed
- maxDurationMs: Native automatically hides splash after maximum time
- Both work even if JavaScript hasn't loaded yet
License
MIT

