
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.
10.x.x
installednpm
or yarn
0.60.x
or aboveTo 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.
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
.
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.
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
.
To begin, let us set up a mock screen in App.js
to reflect the below result.
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
.
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
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
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.
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.
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.
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.
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.
Originally published:
April 28, 2020