DEV Community

Cover image for How to add a splash screen to a React Native app - The easy way
Mateo Hrastnik for Lloyds digital

Posted on • Edited on

How to add a splash screen to a React Native app - The easy way

A splash screen is the first screen the users see after tapping the app icon. It's typically a simple screen with your app logo in the center and goes away once the app is ready to launch.

There are two popular packages for adding a splash screen to your app. Namely react-native-splash-screen and expo-splash-screen. While both of them work, the problem is that they're complicated to install and control. There's a much simpler way to add a splash screen and I'm going to show you how.

The idea is to move the splash screen from native code to Javascript. This will give us more control over the rendering, plus we can have matching splash screens on both platforms. We're going to do a couple of things:

  1. Manually set a blank, single-color splash screen background on the native side
  2. On iOS, set the background color of the React Native root view to that same color
  3. As soon as React Native loads, add a View with the same color in React Native and fade in the app logo on it
  4. Once the app loads, fade out the splash screen

The idea is to show the same color screen while the app boots up and React Native initializes. This is typically really short. Then we fade in the splash screen from our React Native code, and while the splash screen is visible, we can initialize the app, fetch assets, load configuration files, and everything else.

Here's what the final product looks like:

App with splash screen

App with splash screen

And here's a GitHub repo with a blank app with the added splash screen functionality.

https://github.com/hrastnik/react-native-splash-screen-example

Prerequisites

Pick a background color and get the hex value and RGB values in range 0-1. In my example I picked #E0B9BB. The RGB values are 0.87843, 0.72549 and 0.73333. You can use this converter to grab the RGB values easily.

Now pick an image you'll show in the center of the splash screen. This can be the logo or the app icon or anything else. In my case, it's a PNG of my cat.

Setting a blank background on iOS

Open AppDelegate.mm and change add the following line:



return [super application:application didFinishLaunchingWithOptions:launchOptions];


Enter fullscreen mode Exit fullscreen mode

to this:



BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];
self.window.rootViewController.view.backgroundColor = [UIColor colorWithRed:0.87843 green:0.72549 blue:0.73333 alpha:1.00];
return result;


Enter fullscreen mode Exit fullscreen mode

If you're using older versions of React Navigation that don't use AppDelegate.mm file you can open AppDelegate.m and change the lines:



  if (@available(iOS 13.0, *)) {
      rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
      rootView.backgroundColor = [UIColor whiteColor];
  }


Enter fullscreen mode Exit fullscreen mode

to simply say



rootView.backgroundColor = [UIColor colorWithRed:0.87843 green:0.72549 blue:0.73333 alpha:1.0];


Enter fullscreen mode Exit fullscreen mode

Make sure to change the RGB values to match your color.

This changed the background color of the React Native root view, but we still need to change the background of the whole app. You could do this step through XCode but I find it faster to use a text editor. Simply paste this code in your ios/<AppName>/LaunchScreen.storyboard file:



<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
    <device id="retina4_7" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="EHf-IW-A2E">
            <objects>
                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
                    <view key="view" autoresizesSubviews="NO" opaque="NO" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="scaleToFill" id="1L6-XZ-uwR">
                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                        <viewLayoutGuide key="safeArea" id="VQL-mw-5y0"/>
                        <color key="backgroundColor" red="0.87843" green="0.72549" blue="0.73333" alpha="1" colorSpace="deviceRGB"/>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="22" y="100"/>
        </scene>
    </scenes>
</document>


Enter fullscreen mode Exit fullscreen mode

Note that you'll have to change the line that starts with <color key="backgroundColor" and replace the RGB values with your own.

If you run the app at this point, you should see the background color you picked.

App with blank background

App with blank background

Setting a blank background on Android

On Android, all you have to do is add the following line to android/app/src/main/res/values/styles.xml file inside the <style> tag of your app theme:



<item name="android:windowBackground">#E0B9BB</item>


Enter fullscreen mode Exit fullscreen mode

The file should look like this:



<resources>
    <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="android:windowBackground">#E0B9BB</item>
    </style>
</resources>


Enter fullscreen mode Exit fullscreen mode

The JS part

Now let's add the Splash component. It works like this:

  1. At first, it shows only a blank View with the background color we picked. This is visible until the splash screen image is loaded into memory
  2. The image fades in
  3. We wait in this state until the app is initialized and ready to run
  4. The whole splash screen fades out, revealing the app's first screen
  5. Finally, the splash screen assets are cleaned up


import React, { useEffect, useRef, useState } from "react";
import { Animated, StyleSheet } from "react-native";

export function WithSplashScreen({
  children,
  isAppReady,
}: {
  isAppReady: boolean;
  children: React.ReactNode;
}) {
  return (
    <>
      {isAppReady && children}

      <Splash isAppReady={isAppReady} />
    </>
  );
}

const LOADING_IMAGE = "Loading image";
const FADE_IN_IMAGE = "Fade in image";
const WAIT_FOR_APP_TO_BE_READY = "Wait for app to be ready";
const FADE_OUT = "Fade out";
const HIDDEN = "Hidden";

export const Splash = ({ isAppReady }: { isAppReady: boolean }) => {
  const containerOpacity = useRef(new Animated.Value(1)).current;
  const imageOpacity = useRef(new Animated.Value(0)).current;

  const [state, setState] = useState<
    | typeof LOADING_IMAGE
    | typeof FADE_IN_IMAGE
    | typeof WAIT_FOR_APP_TO_BE_READY
    | typeof FADE_OUT
    | typeof HIDDEN
  >(LOADING_IMAGE);

  useEffect(() => {
    if (state === FADE_IN_IMAGE) {
      Animated.timing(imageOpacity, {
        toValue: 1,
        duration: 1000, // Fade in duration
        useNativeDriver: true,
      }).start(() => {
        setState(WAIT_FOR_APP_TO_BE_READY);
      });
    }
  }, [imageOpacity, state]);

  useEffect(() => {
    if (state === WAIT_FOR_APP_TO_BE_READY) {
      if (isAppReady) {
        setState(FADE_OUT);
      }
    }
  }, [isAppReady, state]);

  useEffect(() => {
    if (state === FADE_OUT) {
      Animated.timing(containerOpacity, {
        toValue: 0,
        duration: 1000, // Fade out duration
        delay: 1000, // Minimum time the logo will stay visible
        useNativeDriver: true,
      }).start(() => {
        setState(HIDDEN);
      });
    }
  }, [containerOpacity, state]);

  if (state === HIDDEN) return null;

  return (
    <Animated.View
      collapsable={false}
      style={[style.container, { opacity: containerOpacity }]}
    >
      <Animated.Image
        source={require("~/assets/splash.png")}
        fadeDuration={0}
        onLoad={() => {
          setState(FADE_IN_IMAGE);
        }}
        style={[style.image, { opacity: imageOpacity }]}
        resizeMode="contain"
      />
    </Animated.View>
  );
};

const style = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: '#E0B9BB',
    alignItems: "center",
    justifyContent: "center",
  },
  image: {
    width: 250,
    height: 250,
  },
});



Enter fullscreen mode Exit fullscreen mode

Note that you must change the colors and the path to your image asset to match your project.

You could add more content to your splash screen if you feel it's necessary.

Lastly, we need to actually use this component in our app. My app entry point looks like this, and you'll probably have something similar. The idea is to wrap your app entry point in the WithSplashScreen component and once the app is initialized, set the isAppReady prop to true.



export function AppEntry() {
  const store = useRef(undefined);
  const queryClient = useRef(undefined);

  const [isAppReady, setIsAppReady] = useState(false);

  useEffect(() => {
    initialize().then((context) => {
      store.current = context.store;
      queryClient.current = context.queryClient;

      setIsAppReady(true);
    });
  }, []);

  return (
    <WithSplashScreen isAppReady={isAppReady}>
      <StoreProvider store={store.current}>
        <QueryClientProvider client={queryClient.current}>
          <SafeAreaProvider>
            <KeyboardAvoidingView>
              <ModalProvider>
                <Router />
              </ModalProvider>
            </KeyboardAvoidingView>
          </SafeAreaProvider>
        </QueryClientProvider>
      </StoreProvider>
    </WithSplashScreen>
  );
}


Enter fullscreen mode Exit fullscreen mode

If you did everything right, the end result should look something like this:

App with splash screen

App with splash screen

Conclusion

Instead of adding complex native dependencies that are hard to control, it's easy to add a splash screen to your app with just a couple of changes on the native side. As a benefit, you get extra control over how the splash screen looks and behaves.

Thanks for giving this article a read! If you've found it useful, consider liking and sharing.

Lloyds digital is available for partnerships and open for new projects. If you want to know more about us, check us out.

Top comments (20)

Collapse
 
garrettm30 profile image
Garrett M

I am now using React Native 0.71. Most of these instructions still work great, but I am stuck at the part about editing AppDelegate.m (Under the heading “Setting a blank background on iOS” above), as that file no longer exists with RN 0.71. Instead, there is AppDelegate.mm, and there is nothing in that file that matches the instructions. I tried just adding the suggested line in the file, but it failed to compile, and I don’t know what I’m doing in that file anyway.

I was able to complete all the other steps, and it works perfectly on Android. It’s nearly there on iOS: it launches with the appropriate blank color as defined in LaunchScreen.storyboard, then it briefly flashes to white, then it goes on correctly to the code created in “The JS Part.” I would like to get rid of the flash of white, and I suspect it relates to the step about editing AppDelegate.m that I can’t figure out in this version of React Native.

Does anyone have a suggestion?

Collapse
 
hrastnik profile image
Mateo Hrastnik

Yes, I've updated the article now.

You can use the following line to change the color of the native view and avoid the blank screen:

self.window.rootViewController.view.backgroundColor = [UIColor colorWithRed:0.98431 green:0.78824 blue:0.04314 alpha:1.00];
Enter fullscreen mode Exit fullscreen mode
Collapse
 
garrettm30 profile image
Garrett M

It is so kind of you to respond. I’m afraid I still can’t figure out where to put that line, since I have no AppDelegate.m file in this project that was newly started from React Native 0.71. I have also done a search across my entire code base for self.window.rootViewController as well as rootView.backgroundColor and I got no results. I tried adding the line you suggested above to various places in AppDelegate.mm, AppDelegate.h and main.m just to see if it was as simple as that, but in each case, the app could not compile.

In the end, I am not sure that it is necessary. Following the rest of your steps (many thanks, by the way), the flash of the default color appears only to happen on dev builds of the app. On a release build, the app launches with my desired color from the beginning and moves smoothly into the splash screen as I would hope.

Thread Thread
 
hrastnik profile image
Mateo Hrastnik • Edited

Open AppDelegate.mm and change the following line:

return [super application:application didFinishLaunchingWithOptions:launchOptions];
Enter fullscreen mode Exit fullscreen mode

to this:

BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];
self.window.rootViewController.view.backgroundColor = [UIColor colorWithRed:0.87843 green:0.72549 blue:0.73333 alpha:1.00];
return result;
Enter fullscreen mode Exit fullscreen mode

I've updated the article. Also there's a slight change in the LaunchScreen.storyboard file. You can change the colorSpace of your background to use device RGB (colorSpace="deviceRGB") otherwise the colors will be slightly different between the native to JS handoff.

Thread Thread
 
garrettm30 profile image
Garrett M

Thank you for the followup. I have now implemented this last change with success. I also thank you for updating your article so it stays relevant.

Collapse
 
esegebart profile image
Elyse Segebart • Edited

I am trying to understand this part.

const [state, setState] = useState<
| typeof LOADING_IMAGE
| typeof FADE_IN_IMAGE
| typeof WAIT_FOR_APP_TO_BE_READY
| typeof FADE_OUT
| typeof HIDDEN

(LOADING_IMAGE);

When I try to use this, I get an error saying unexpected token pointing to the | in front of the first typeof. How are you able to do that?

Collapse
 
blwinters profile image
Ben Winters

For this I converted the constants to a TS enum:
enum SplashState {
Loading,
FadeIn,
WaitForReady,
FadeOut,
Hidden,
}
// ...
const [state, setState] = useState<SplashState>(SplashState.Loading)

Or you could use a TS union type of the string constants.

Collapse
 
srikarsv profile image
Sreekar

Hello Everyone,
We got issues in splash screen i.e., white screen is coming after the splash screen instead of redirected to the login screen. And it took more time to load the index.js file.So, that during transition from splash screen to login screen in between white screen.
Could you please help us out as soon possible?

Collapse
 
mdehghani65 profile image
mdehghani65

I translated the splash component code as follows:

import React, { useEffect, useRef, useState } from "react";
import { Animated, StyleSheet } from "react-native";

export function WithSplashScreen(props) {
return (
<>
{props.isAppReady && props.children}

  <Splash isAppReady={props.isAppReady} />
</>
Enter fullscreen mode Exit fullscreen mode

);
}

const LOADING_IMAGE = "Loading image";
const FADE_IN_IMAGE = "Fade in image";
const WAIT_FOR_APP_TO_BE_READY = "Wait for app to be ready";
const FADE_OUT = "Fade out";
const HIDDEN = "Hidden";

export const Splash = (isAppReady = props.isAppReady) => {
const containerOpacity = useRef(new Animated.Value(1)).current;
const imageOpacity = useRef(new Animated.Value(0)).current;

const [state, setState] = useState(LOADING_IMAGE);

useEffect(() => {
if (state === FADE_IN_IMAGE) {
Animated.timing(imageOpacity, {
toValue: 1,
duration: 1000, // Fade in duration
useNativeDriver: true,
}).start(() => {
setState(WAIT_FOR_APP_TO_BE_READY);
});
}
}, [imageOpacity, state]);

useEffect(() => {
if (state === WAIT_FOR_APP_TO_BE_READY) {
if (isAppReady) {
setState(FADE_OUT);
}
}
}, [isAppReady, state]);

useEffect(() => {
if (state === FADE_OUT) {
Animated.timing(containerOpacity, {
toValue: 0,
duration: 1000, // Fade out duration
delay: 1000, // Minimum time the logo will stay visible
useNativeDriver: true,
}).start(() => {
setState(HIDDEN);
});
}
}, [containerOpacity, state]);

if (state === HIDDEN) return null;

return (
collapsable={false}
style={[style.container, { opacity: containerOpacity }]}
>
source={require('../images/bluecircle.png')}
fadeDuration={0}
onLoad={() => {
setState(FADE_IN_IMAGE);
}}
style={[style.image, { opacity: imageOpacity }]}
resizeMode="contain"
/>

);
};

const style = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#E0B9BB',
alignItems: "center",
justifyContent: "center",
},
image: {
width: 250,
height: 250,
},
});

Collapse
 
dogisdev profile image
Golden Retriever

Hey. What's this function you're using initialize()?

Collapse
 
hrastnik profile image
Mateo Hrastnik

I use it to initialize the app and run stuff I need to do on app start.

For example it could look something like this:

async function initialize() {
  await loadFonts()
  const store = createReduxStore();
  await initializePushNotifications()
  ...
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
avidlearner3 profile image
avidlearner

Hi! I wanted to attach video to the splash screen background I am from JS world so have little to no knowledge of objective c and all that.

Collapse
 
tarajima profile image
Tara Nakajima

This works pretty great but does anyone else have an issue with the splash screen being a bit blurry on Android?

Collapse
 
hrastnik profile image
Mateo Hrastnik

Just make sure the image you use has a big enough resolution

Collapse
 
matti profile image
Matti_64

Thanks it works great on iOS but on android the background color is popping up during navigation (probably due to android:windowBackground set to AppTheme in styles.xml)

Collapse
 
matti profile image
Matti_64 • Edited

Ok the issue is linked to Stack.Navigator V6..
to fix it, i've added a screenOptions param to Stack.Navigator
<Stack.Navigator screenOptions={{ presentation: 'transparentModal', contentStyle: { backgroundColor: 'white' } }}>

Collapse
 
ialphaomegai profile image
IAlphaOmegaI

Hello everyone!
The image is not appearing for me even though I have placed the right path, anyone that can help?
Thanks in advance!

Collapse
 
linhvo profile image
Linh Vo

awesome thank you