Dark mode is a great way to enhance the user experience for a mobile app. Many commonly used and famous applications have support for dark mode now. iOS and Android added dark mode support to their platforms within the last year, which means that it's easier than ever to support this feature in your app.

In this tutorial, let's create a demo app that is going to change its appearance from light to dark theme based on the settings provided by the mobile OS as default. To create a small theme and detect system settings, you are also going to use two libraries called styled-components and react-native-appearance. The latter package allows access to operating system information and detecting color schemes.

Requirements

  • Nodejs version <= 10.x.x installed
  • watchman installed
  • have access to one package manager such as npm or yarn
  • use react native version 0.60.x or above

Getting started with the Crowdbotics App Builder

To generate a new React Native project, you can use the react-native cli tool. Or, if you want to follow along, I'll show you how to generate a new app using the Crowdbotics App Builder.

Create a project page in Crowdbotics

Make sure you have login access to Crowdbotics' App Builder. You can register using either your GitHub credentials or your email. Once logged in, you can click Create App to create a new app. The next screen is going to prompt you for what type of application you want to build. Choose the mobile app.

App type selection page in Crowdbotics

Enter the name of the application and click the button Create App. After you link your GitHub account from the dashboard, you are going to have access to the GitHub repository for the app. This repo generated uses the latest react-native version and comes with built-in components and complete examples that can be the base foundation for your next app.

That's it. It's an easy, three-step easy process. Now, let us get back to our tutorial.

Configure react-native-appearance

To start, install the dependency itself. The package react-native-appearance is actively maintained by Expo and is available to use both in Expo apps and vanilla React Native apps (apps generated using the react-native cli tool).

Open a terminal window, make sure you are inside the project directory, and install the following dependency.

yarn add react-native-appearance

For iOS devices, to configure and use it correctly, enter the below commands to install pods.

cd ios/
pod install

For Android devices, there is no specific command to bind the native binaries. It is a two-step process. First, open android/app/src/main/AndroidManifest.xml and add a uiMode flag.

android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode">

Then, open another file called android/app/src/main/java/com/rnDarkModeStyledComponentsDemo/MainActivity.java and add the following:

import android.content.Intent;
import android.content.res.Configuration;

// inside public class MainActivity extends ReactActivity
@Override
 public void onConfigurationChanged(Configuration newConfig) {
 super.onConfigurationChanged(newConfig);
 Intent intent = new Intent("onConfigurationChanged");
 intent.putExtra("newConfig", newConfig);
 sendBroadcast(intent);

That's it to configure the module react-native-appearance.

Installing the styled-components library

To begin, let us set up a mock screen in App.js to reflect the below result.

Default screen with the text "Crowdbotics app

Open the App.js file and add the following code.

import React from 'react'
import { View, Text, StyleSheet } from 'react-native'

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Crowdbotics app</Text>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
})

Now, go back to the terminal window and install the styled-components library.

yarn add styled-components

If you are familiar with styled-components, do note that it can be used with React Native in the same way as on the web. You just have to import the styled utility to create components from styled-components/native.

To see it in action, let us convert the App component by replacing both the View and Text with Container and Title. These new elements are going to be custom using semantics from styled-components.

import React from 'react'
import styled from 'styled-components/native'

const Container = styled.View`
  flex: 1;
  background-color: #fff;
  align-items: center;
  justify-content: center;
`

const Title = styled.Text`
  font-size: 24;
`

export default class App extends React.Component {
  render() {
    return (
      <Container>
        <Title>Crowdbotics app</Title>
      </Container>
    )
  }
}

In the above snippet, you can notice that styled-components utilizes tagged template literals to style your components using backticks. When creating a component in React or React Native using styled-components, each component is going to have styles attached to it. Note that the Container is a React Native View and has styling attached to it, and similarly for Title as well as Text.

One advantage styled-components provides is that it uses the same Flexbox model as React Native Layouts. The advantage here is that you get to use the same understandable syntax that you have been using in web development and standard CSS.

Here is the output after adding styled-components.

Placeholder screen with updated styling

Defining themes

In this section, let us define two basic themes in two separate files. The app will toggle between these files using a theme manager provided by styled-components.

Create a new directory src/themes/ with two file names: light.js and dark.js.

Open light.js to define a basic set of colors to be used when the light theme is active. The value of themes is going to be inside a JavaScript object. Add the following snippet to it.

const light = {
  theme: {
    background: '#ededed',
    border: '#bdbdbd',
    backgroundAlt: '#eaeaeb',
    borderAlt: '#bdbdbd',
    text: '#171717'
  }
}

export default light

Next, open dark.js and similarly add theme values.

const dark = {
  theme: {
    background: '#2E3440',
    border: '#575c66',
    backgroundAlt: '#575c66',
    borderAlt: '#2E3440',
    text: '#ECEFF4'
  }
}

export default dark

Add a theme manager using context

The styled-components library provides a way to handle different themes in a React Native app using a ThemeManager. It listens to theme changes and, at the same time, allows the user to make a change to the appearance of the app, either manually by toggling, or by setting a default theme (which is handled by react-native-appearance).

Create a new file called index.js inside src/themes/ and start by importing the following statements inside. We are going to use React Hooks to set and change the value of themes. This can be done by setting a default theme value.

Next, let us also change the StatusBar color depending on the theme value.

import React, { createContext, useState, useEffect } from 'react'
import { StatusBar } from 'react-native'
import { ThemeProvider } from 'styled-components/native'
import { Appearance, AppearanceProvider } from 'react-native-appearance'

import lightTheme from './light'
import darkTheme from './dark'

The last two import statements are the theme files. Define a defaultMode variable whose value is either going to be based on the OS theme selection or the default theme value provided by you in the app. Using Appearance.getColorScheme() from react-native-appearance the mobile OS's theme value can be fetched.

const defaultMode = Appearance.getColorScheme() || 'light'

Create a ThemeContext that is going to hold the value of the current theme (or mode) and a helper function to change that value.

The useTheme is going to be a helper function that uses the ThemeContext. Do not forget to export it, since you will be using it directly in the UI component later.

const ThemeContext = createContext({
  mode: defaultMode,
  setMode: mode => console.log(mode)
})

export const useTheme = () => React.useContext(ThemeContext)

Now, define a ThemeManager Provider that is going to take care of setting the theme, changing the state or mode of the current theme. Using the useEffect hook, it is going to listen to the theme changes made by the operating system.

This listening is managed by adding a subscription using addChangeListener from react-native-appearance.

Also, wrap children of the component inside the ThemeProvider imported from styled-components/native. The children here are going to be the StatusBar component from react-native as well as the other UI components passed as the children prop. The content of the prop is going to be injected from the screen component.

const ManageThemeProvider = ({ children }) => {
  const [themeState, setThemeState] = useState(defaultMode)
  const setMode = mode => {
    setThemeState(mode)
  }
  useEffect(() => {
    const subscription = Appearance.addChangeListener(({ colorScheme }) => {
      setThemeState(colorScheme)
    })
    return () => subscription.remove()
  }, [])
  return (
    <ThemeContext.Provider value={{ mode: themeState, setMode }}>
      <ThemeProvider
        theme={themeState === 'dark' ? darkTheme.theme : lightTheme.theme}>
        <>
          <StatusBar
            barStyle={themeState === 'dark' ? 'dark-content' : 'light-content'}
          />
          {children}
        </>
      </ThemeProvider>
    </ThemeContext.Provider>
  )
}

Lastly, the root of the app has to be wrapped inside the AppearanceProvider to make the OS changes work and listen to mobile OS subscriptions. Do not forget to export the ThemeManager.

const ThemeManager = ({ children }) => (
  <AppearanceProvider>
    <ManageThemeProvider>{children}</ManageThemeProvider>
  </AppearanceProvider>
)

export default ThemeManager

Using themes inside the app

To let the user change the theme of the app, you are going to import the ThemeManager inside App.js. Open the file, and add the following import statements.

import ThemeManager, { useTheme } from './src/themes'
import { Switch } from 'react-native'

The Switch is going to be the component button from the react-native core that allows the user to change the theme manually on a toggle.

To reflect the correct background color as well as the text color, let's use prop values from the theme files to the Container and Title components.

const Container = styled.View`
  flex: 1;
  /* add this */
  background: ${props => props.theme.backgroundAlt};
  align-items: center;
  justify-content: center;
`

const Title = styled.Text`
  font-size: 24;
  /* add this */
  color: ${props => props.theme.text};
`

Now create a HomeScreen component that is going to have the Switch component wrapped inside Container. To toggle between the two themes, it is going to refer to the useTheme helper method.

function HomeScreen() {
  const theme = useTheme()
  return (
    <Container>
      <Title>Crowdbotics app</Title>
      <Switch
        value={theme.mode === 'dark'}
        onValueChange={value => theme.setMode(value ? 'dark' : 'light')}
      />
    </Container>
  )
}

From the above code snippet, notice that the Switch component requires two props: value and onValueChange. The onValueChange callback updates the value prop. If it doesn't update, the default value provided to the value prop continues to render.

Lastly, wrap the HomeScreen component inside ThemeManager to make it work as below.

function App() {
  return (
    <ThemeManager>
      <HomeScreen />
    </ThemeManager>
  )
}

export default App

Here is the output you are going to get, depending on default theme settings in your device or simulator OS.

The app now has a toggle switch added to its screen

Testing the manual toggle

I am going to test this app inside an iOS simulator. By default, the iOS simulator I am running has a dark mode.

Here is the first use case when the user manually switches between the two themes. Notice the changes in the background color of the Home screen and the text color of the title.

The toggle switches the theme's colors

Testing the appearance based on OS theme

To find where you can switch between appearances on an iOS simulator, open Settings, where you'll come across a Developer menu as shown below.

Settings page in iOS simulator

Open that to find the Appearance section. In the below image, you can see it is set to dark mode.

Here is the complete demo. When the OS appearance setting changes, it is directly reflected in our React Native app.

The app's layout changes based on the user's appearance preferences

Conclusion

As you can see, adding dark mode support in React Native apps is straightforward when using the react-native-appearance package. It works for all devices that support dark mode.