Consistent cross-platform Expo splash screen

Archived. Written for Expo SDK 52. Newer SDKs have edge-to-edge built in, so parts of this are out of date.

Finally, after years of android-side bugs and complexities, we can use kind of simple way to show the same splash screen on both platforms. Thanks expo

"dependencies": {
  "expo": "^52.0.40",
  "react-native-edge-to-edge": "^1.4.3",
}

Simple example

The idea is to show icon instead of fullscreen splash screen.

// app.json
{
  "expo": {
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "dark",
    "androidStatusBar": {
      "barStyle": "light-content",
      "backgroundColor": "#1F1F20",
      "translucent": true
    },
    "plugins": [
      "expo-build-properties",
      "react-native-edge-to-edge",
      [
        "expo-splash-screen",
        {
          "backgroundColor": "#1F1F20",
          "image": "./assets/icon.png",
          "resizeMode": "cover",
          "imageWidth": 200
        }
      ]
    ]
  }
}

Use your onReady (fonts, feed fetching, onLayout)

// App.tsx
import * as SplashScreen from "expo-splash-screen";

SplashScreen.preventAutoHideAsync();

const App = () => {
  const [ready, setReady] = useState(false);

  const onReady = () => {
    setReady(true);
    SplashScreen.hide();
  };

  return (
    <GestureHandlerRootView>
      <NavigationContainer onReady={onReady}>
        <StackNavigator />
      </NavigationContainer>

      <AnimatedSplash ready={ready} />

      <SystemBars style="auto" />
    </GestureHandlerRootView>
  );
};

Animate it your way

// AnimatedSplash.tsx
import { Image } from "expo-image";
import { useEffect, useState } from "react";
import { Platform, StyleSheet } from "react-native";
import Animated, {
  Easing,
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withTiming,
} from "react-native-reanimated";

const SPLASH_DELAY = Platform.OS == "android" ? 1000 : 200;
const SPLASH_DURATION = 1000;

export const AnimatedSplash = ({ ready }: { ready: boolean }) => {
  const scale = useSharedValue(1);
  const opacity = useSharedValue(1);
  // const rotation = useSharedValue(0);
  const [isSplashDone, setIsSplashDone] = useState(false);

  const animatedStyles = useAnimatedStyle(() => ({
    transform: [
      { scale: scale.value },
      // { rotateZ: `${rotation.value}deg` }
    ],
    opacity: opacity.value,
  }));

  useEffect(() => {
    if (ready) {
      scale.value = withDelay(
        SPLASH_DELAY,
        withTiming(
          1.5,
          { duration: SPLASH_DURATION, easing: Easing.cubic },
          (finished) => {
            if (finished) {
              runOnJS(setIsSplashDone)(true);
            }
          }
        )
      );

      opacity.value = withDelay(
        SPLASH_DELAY,
        withTiming(0, {
          duration: SPLASH_DURATION,
          easing: Easing.cubic,
        })
      );
    }
  }, [ready]);

  return (
    <>
      {!isSplashDone && (
        <Animated.View style={[animatedStyles, styles.container]}>
          <Image
            style={styles.image}
            contentFit="cover"
            source={require("@assets/icon-transparent.png")}
          />
        </Animated.View>
      )}
    </>
  );
};

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: "#1F1F20",
    justifyContent: "center",
    alignItems: "center",
  },
  image: {
    width: 200,
    height: 200,
  },
});

Other ways