How to make an animated splash screen in React Native and Expo

By the LumioUI team · Updated

The native splash screen can't animate: it's a static image shown before JavaScript runs. To animate it in Expo, keep the native splash up with expo-splash-screen, render a React Native copy that looks identical on top of your app, hide the native one as soon as the copy is on screen, then animate the copy away once fonts and data are ready. The LumioUI Splash Screen does the handoff, holds a fast launch long enough not to flash, and breathes gently on a slow one instead of spinning.

Recording of the LumioUI Splash Screen component (Pro).

How an animated splash screen works

  1. Configure the native splash. Set the image and background colour in app.json (the expo-splash-screen plugin). This is what shows while the app boots.
  2. Don't let it hide on its own. Call preventAutoHideAsync() at the top level, outside any component, so it runs before the first render.
  3. Draw a seamless copy. Render a full-screen view with the same background and the logo in the same size and position. Hide the native splash in its onLayout: the swap is invisible.
  4. Wait for ready. Fonts, the saved session and the first screen's data. Hold for a minimum time too, so a fast launch doesn't flash the logo for one frame.
  5. Animate it away. Fade the cover while the logo scales up, or reveal the app section by section, then unmount the cover so it can't block touches.
TSX AnimatedSplash.tsx
import * as React from "react";
import { Image, StyleSheet, View } from "react-native";
import * as SplashScreen from "expo-splash-screen";
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";

// Keep the native splash up until our matching React Native copy is on screen.
SplashScreen.preventAutoHideAsync();

export function AnimatedSplash({ ready, children }: { ready: boolean; children: React.ReactNode }) {
  const [done, setDone] = React.useState(false);
  const fade = useSharedValue(1);
  const scale = useSharedValue(1);

  React.useEffect(() => {
    if (!ready) return;
    scale.value = withTiming(1.15, { duration: 400 });
    fade.value = withTiming(0, { duration: 400 }, (finished) => {
      if (finished) runOnJS(setDone)(true);
    });
  }, [ready]);

  const cover = useAnimatedStyle(() => ({ opacity: fade.value }));
  const logo = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));

  return (
    <View style={{ flex: 1 }}>
      {children}
      {!done && (
        <Animated.View style={[styles.cover, cover]} pointerEvents="none"
          // Same background and logo position as the native splash: no seam.
          onLayout={() => SplashScreen.hideAsync()}>
          <Animated.View style={logo}>
            <Image source={require("./assets/splash-icon.png")} style={{ width: 120, height: 120 }} />
          </Animated.View>
        </Animated.View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  cover: {
    position: "absolute", top: 0, right: 0, bottom: 0, left: 0,
    alignItems: "center", justifyContent: "center", backgroundColor: "#ffffff",
  },
});

Respect reduced motion: when it's on, keep the fade and drop the scale.

The quick way: LumioUI Splash Screen

The Splash Screen wraps your app, holds until ready and a minimum time have both passed, then hands off in one of four styles and unmounts. It's Expo Go compatible and part of LumioUI Pro.

TSX App.tsx
import * as React from "react";
import * as NativeSplash from "expo-splash-screen";
import { useFonts } from "expo-font";
import { SplashScreen } from "@/components/ui/splash-screen/splash-screen";
import { Logo } from "./Logo";

NativeSplash.preventAutoHideAsync();

export default function App() {
  const [fontsLoaded] = useFonts({ /* your fonts */ });
  const session = useSession(); // undefined while loading
  // The Lumio splash covers the app from its first frame, so the native
  // splash can go as soon as React has rendered.
  React.useEffect(() => { NativeSplash.hideAsync(); }, []);

  return (
    <SplashScreen
      ready={fontsLoaded && session !== undefined}
      mark={<Logo />}
      animation="light"
    >
      <SplashScreen.Band index={0}><Header /></SplashScreen.Band>
      <SplashScreen.Band index={1}><Feed /></SplashScreen.Band>
    </SplashScreen>
  );
}
  • ready starts the handoff; mark is your logo, sized as on the native splash.
  • animation is "light" (default), "zoom", "fade" or "curtain". minDuration (700ms) stops a fast launch from flashing.
  • slowAfter (2s) switches to a breathing light, or your loadingIndicator, on slow launches. footer holds a "from Company" line.
  • SplashScreen.Band reveals your first screen in sections (header, hero, content) instead of all at once. onHidden fires when it's gone.

Add it to your app

LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:

npx @lumioui/cli init
npx @lumioui/cli add pro/splash-screen

It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Splash Screen API.Splash Screen is part of LumioUI Pro: Pro installs need your licence key (CLI guide, pricing).

More React Native guides