Theming with Dark Mode in React Native

How to implement theming in React Native Apps

iOS and Android now have OS level capabilities to switch between dark and light mode, and consumers are now expecting their apps to match this system level theming. However, being able to switch themes in-app too, independent of the OS level, is also important — I have often found myself opting for a dark themed app on top of a light themed phone OS. Beyond dark and light mode, theming in general is an important factor of many apps where users are able to switch between colour schemes and styling options.

This article will cover how to implement capable theming support in your React Native projects taking the above points into consideration. It will cover:

  • How to take the currently used theme from the OS level to refer to in your app projects, that will either be either dark or light
  • Built-in theming support with react-navigation, that will be useful for speeding up theming in your app. There are a small number of colours that have been integrated into the React Navigation components, but it is far from a comprehensive app theming solution
  • How to effectively build your own theme on top of react-navigation’s offering, with maintainability in mind
  • Creating a Theme Manager in the form of React Context, that will provide app components the current theme, and a toggle() function to switch between themes
  • Finally, we’ll explore how to apply themes to components like View and Text, as well as how to persist the current theme for subsequent app sessions if you choose to do so

This article is focussed on theming in React Native — the solutions of which differ greatly to React for the web. If you are looking for a comprehensive guide to implementing theming for web-based React, visit my article dedicated to doing so:
Go to React Dark Mode with Styled Theming and Context

Where to start with React Native theming? We firstly need to know what theme is applied to the app on the OS-level, and apply that to the default theme when opening the app.

Getting a device’s currently applied theme

Ascertaining a device’s current theme is essential for applying the correct theme to your app when a user opens it. We can do this with a handy packaged named react-native-appearance. The package supplies a simple API to get the theme through a provider component, that will either be light or dark.

A provider is common terminology in React for a context provider — a component that wraps other components to provide them with data the context holds.

Simply wrap react-native-appearance’s <AppearanceProvider /> component around your entire app to provide the theme data we need:

import { AppearanceProvider } from 'react-native-appearance'export function App () { return (
<AppearanceProvider>
<Entry />
</AppearanceProvider>
);
}
export default App;

Now in any child component we can get the theme, or “colour scheme” as the package terms it, with the Appearance object. Also provided by react-native-appearance is a useColorScheme() hook, that reacts to a theme toggle at the device level:

// in some child component file 
import { Appearance, useColorScheme } from 'react-native-appearance'
const theme = Appearance.getColorScheme();export function ChildComponent () {
const colorScheme = useColorScheme();
return (
...
);
}

Notice that Appearance.getColorScheme() returns the currently applied theme to the app, and that’s it — it will not react to changes made. Because of this, we can call this method from anywhere in your project; even outside components.

The useColorScheme() hook on the other hand can only be used in functional components, but does react to changes in the theme of your app.

useColorScheme() sounds very useful upon a first glance, but if your app integrates its own theming solution, with the ability to toggle between them, then useColorScheme() becomes quite redundant — we don’t want the device’s theme overwriting our custom set theme every time it changes.

Perhaps a fair conclusion to make is to use the useColorScheme() hook if your app does not have its own theming solution in place — an app that solely relies on the device’s currently applied theme. If however you intend users to toggle between your own custom-designed themes, it may be a good idea to only use Appearance.getColorScheme() when your app is initialising, in order to get that default theme value only.

That being said, useColorScheme() gives us a very elegant solution when relying on react-navigation’s built-in theming support.

Built-in theming with React Navigation

Your app has ascertained which theme to apply based on a device’s settings — great. Now we need to use that theme value within our internal components to determine the correct styling. This is somewhat simple when used in conjunction with react-navigation; it provides some theming support built in, via a theme prop used with your stack navigators.

To pass the current theme into react-navigation, pass the colour scheme into the theme prop of your main stack navigator, as the following component demonstrates:

// using `useColorScheme` hook and passing theme into a navigatorimport React from 'react'
import { createAppContainer } from 'react-navigation'
import { Navigator } from './Navigator'
import { useColorScheme } from 'react-native-appearance'
const AppNavigator = createAppContainer(Navigator);export function Entry () { const colorScheme = useColorScheme(); return (
<AppNavigator theme={colorScheme} />
);
}
export default Entry;

We have now imported our main navigator stack from a Navigator file, and have initialised our app container with AppNavigator — nothing new here. The additional step taken is that we’ve injected the colorScheme into our navigator with the theme prop.

Now whenever the device’s theme is changed, <Entry /> will re-render, and thus the entire app will update to the correct theming.

The following example introduces an <Entry /> component. In the first example we wrapped <Entry /> with <AppearanceProvider /> within the App component file. I find the convention of wrapping your entire app with providers in App, and initialising those provider values within Entry.

// React Native initialisation pattern<App /> <- AppearanceProvider, ThemeManager, etc
<Entry /> <- get colour scheme, get theme, etc

This is also the case in web-based React, but we often wrap provider components within the index.tsx file, and carry out further initialisation in App.tsx. In bare-bones React Native and Expo projects, <App /> is the top-level component.

With the navigator now aware of what theme to use, we can use it to determine specific styling on custom components. But here’s the catch — react-navigation already automatically updates it’s built-in components based on this theme prop. Because of this, it is worth testing your app as soon as you have provided a theme prop — perhaps you will not need to manipulate your navigators any further.

What are the specific colours used for this automatic theming? Some very generic shades we have come to expect with iOS and Android UI. They can be found within the ThemeColors import:

// inspecting theme colours of react navigationimport { ThemeColor } from 'react-navigation';// light theme colours
console.log(ThemeColors['light']);
// dark theme colours
console.log(ThemeColors['dark']);

Logging either the dark or light index of ThemeColors will present a small collection of colours, that have been closely matched with the operating systems in question — iOS and Android. This gives your app some authenticity for the platforms out of the box.

But what if you do want to customise your navigators based on theme? Well, theme is passed into functions such as navigationOptions and defaultNavigationOptions for us to utilise.

The easiest way to access the theme prop is by using destructure syntax within navigationOptions. From here we can manipulate navigator configurations:

// customising a navigator based on `theme`const MyNavigator = createStackNavigator({
Home: {
screen: Home,
navigationOptions: ({ theme }) => (
headerShown: true,
headerTitle: 'Welcome!',
headerStyle: {
borderBottomColor: theme.light ? '#ccc' : '#333'
},
headerTintColor: theme.light ? '#222' : '#EEE',
)
}
});

This provides some level of customisation within Navigators. What react-navigation also provides is a useTheme hook for functional components, or ThemeContext object for class components, to access the theme within your screen components themselves. Getting theme within functional components:

// importing `theme` into functional componentimport { useTheme } from 'react-navigation'export const MyFunctionalComponent = (props) => {
const theme = useTheme();
return (...);
}

For class components, the static contextType is used instead:

// importing theme into class componentimport { ThemeContext } from 'react-navigation'export class MyClassComponent extends React.component {
static contextType = ThemeContext;
render() {
const theme = this.context;

return(...);
}
}

Either way, theme is reachable throughout your component tree, and so are the ThemeColors array provided with react-navigation.

React Navigation have a dedicated documentation page on theming that can be found here.

So far we have been exploring built-in theming support. Let’s now explore how to define your own custom React Native theming. We’ll discuss how to efficiently define your own themes firstly, before moving onto providing that theme through a React Context.

Defining your own Custom Themes

The built-in support we’ve discussed so far is great, and is undoubtedly a time saver for us developers. But for more customisation, we can turn to implementing our own theming solution, while leveraging built-in support at the same time.

Defining themes should be done within a dedicated file in the top level of your project directory. In the case of this piece, we’ll name this file theme.tsx. We will ultimately want to define our own set of ThemeColors, but we can import react-navigation’s colours for use in our own themes, too:

// theme.tsimport { ThemeColors as ReactNavigationThemeColors } from 'react-navigation'

There are two popular conventions used for defining themes. The first is to separate the themes into separate objects completely:

Early disclosure: This is not my favoured approach, but you will find this method being used in projects, so it is worth covering.

// option 1, separate object definitions for each themeexport const ThemeColors = {
light: {
primaryText: 'black',
primaryBackground: 'white'
...
},
dark: {
primaryText: 'white',
primaryBackground: 'black'
...
}
...
}

This solution works — we just need to refer to either ThemeColors.light or ThemeColors.dark to get to our theme values. However, maintaining these objects as your themes grow will become troublesome and time consuming:

  • If a field is deleted from one theme, then it must also be deleted from the others, too. This task is susceptible to be forgotten in fast-paced development
  • The ordering of each field is often neglected, resulting in poor readability from one theme to the next.
  • It’s just impossible to see the light and dark values of a particular field side by side. If I want to change both values, i’ll need to either scroll through or CMD+F to find that particular field. It would be useful if both values were together

Let’s do something more intelligent, but slightly more complex. Let’s firstly group values by theme property, and then create a helper function to build an exportable object just containing the currently selected theme. This is how the theming is defined:

// defining theme values by theme propertyexport const ThemeColors = {
primaryText: {
light: 'black',
dark: 'white',
...
},
primaryBackground: {
light: 'white',
dark: 'black',
...
},
...
};

This slight restructuring of a theme definition now gives a separate object for each theme property, where values for each theme is defined. With the values for each property now grouped together, it becomes a lot easier to make changes to a particular property.

This method of grouping styles for particular theme fields has also been adopted with styled-theming for web-based React — used in conjunction with styled-components — that I also enjoy using.

As this object is not exportable friendly (we want to export only the currently selected theme, not all the themes), we’ll want a helper function to export a more developer-friendly object:

// getTheme() constructs a usable theme objectexport const getTheme = (mode: string) => {   let Theme = {};   for (let key in ThemeColors) {
Theme[key] = ThemeColors[key][mode];
}
return Theme;
};

Now, this is the function we’ll be importing into a theme manager later in the article to obtain a certain theme object. It takes one argument being the current theme (light or dark), and constructs a new object with only that theme configuration:

// getTheme constructs an object for a particular themegetTheme(light);> {
ThemeColors.primaryText.light,
ThemeColors.primaryBackground.light,
...
}

Not only do we get an easy-to-use object with this method with getTheme(), we also make maintaining the theme easier by the more sophisticated grouping method.

Here is a templated theme.tsx with the above solution in place:

So with a theme now defined, it needs to be given to the rest of your app to use. Let’s next explore a <ThemeManager /> component that will define a custom React Context to store the currently selected theme, as well as a toggle() function for switching between them.

Creating a Custom Theme Manager

This is where things overlap with web-based React; implementing a Theme Manager for React Native will be almost identical to web React. Nevertheless, the solution will be explored here.

One difference from web React is that we can factor in the device theme (via react-native-appearance discussed earlier) when initialising the React Context.

Below is the completed <ThemeManager /> component. We’ll discuss this implementation next:

In around 50 line of code, React Native now has a means to refer to a custom theme and toggle that theme, too. Starting at the top, we have imported the currently used device theme, via react-native-appearance once again, and stored it in an osTheme variable.

osTheme is used within the default value of the ManageThemeContext Context, along with the getTheme() function defined previously to generate the currently selected theme object:

// defining a theme manager contextimport { Appearance } from 'react-native-appearance'
import { getTheme } from '../theme'
const osTheme = Appearance.getColorScheme();export const ManageThemeContext: React.Context<any> = React.createContext({
mode: osTheme,
theme: getTheme(osTheme)
toggle: () => { }
});

Note that the toggle field is an empty function — the implementation of toggle() is done within the <ThemeManager /> component, and is applied to the context provider.

ManageThemeContext is exportable, and can be used within class components to refer to the theme via the static contextType value mentioned earlier. For functional components though, another useTheme() hook has been defined using React’s useContext hook:

// define useTheme hook for functional componentsexport const useTheme = () => React.useContext(ManageThemeContext);

Local state has has been defined within <ThemeManager /> consisting only of a mode field, referring to the currently selected theme. Using toggle(), we can update this state and therefore the selected theme, that will cause the entire app to re-render and thus reflect the updated theming values throughout.

// toggling themes changes state, causing an app re-renderstate = {
mode: osTheme
};
toggleTheme = async () => {
this.state.mode === 'light'
? this.setState({
mode: 'dark'
})
: this.setState({
mode: 'light'
})
}

Notice that our default value is osTheme.

With the means of using the theme within components covered, the ThemeManger component is then defined. <ThemeManager /> wraps the entire application, providing it with our theme context. Within render, we simply wrap props.children with ManageThemeContext.Provider:

<ManageThemeContext.Provider value={{
mode: this.state.mode,
theme: getTheme(this.state.mode),
toggle: this.toggleTheme
}}>
{this.props.children}
</ManageThemeContext.Provider>

The key here is that values within the context are referring to the state theme. Had we simply referred to osTheme, we’d never see a state update, and therefore re-render upon switching themes.

To provide the entire component tree with this custom theme manager, App is an ideal place to import <ThemeManager />, to wrap your entire project with:

// wrapping entire project with ThemeManager within Appimport { AppearanceProvider } from 'react-native-appearance'
import { ThemeManager } from './ThemeManager'
export function App () {return (
<AppearanceProvider>
<ThemeManager>
<Entry />
</ThemeManager>
</AppearanceProvider>
);
}
export default App;

Every component under <ThemeManager /> can now utilise the useTheme() hook, as well as the ManageThemeContext object to access the current theme.

Embedding context values in components

With ManageThemeContext.Provider now hosting the toggle() function, any component under it can call toggle() to switch themes. On top of this, the mode and theme values are also available to components, giving the currently selected theme and entire theme object respectively.

To demonstrate how to access these values, the following <ThemeSettings /> component utilises everything our theme context offers.

  • mode is displayed in the first Text element to display the currently selected theme
  • theme values are embedded within style props to actually change the appearance of components. theme.primaryText has been embedded here
  • toggle() has been used within an onPress() event handler to make theme switching possible

The full component is as follows:

In Summary

In this piece we have explored both the built-in theming solutions, courtesy of react-navigation, as well as a custom theming solution utilising React Context, hooks, and a <ThemeManager /> component as the context provider.

The reader should now have a solid grasp on how to implement custom designed themes within their React Native apps. Here are some further considerations:

  • Persist the currently selected theme in-app using React Native’s AsyncStorage API. Storing the current theme in AsyncStorage will persist it through subsequent app sessions, and may be useful when implementing customised theming to your apps

I have discussed AsyncStorage in more detail in another authentication focused piece on React Native, that can be found here:

  • The theming configuration objects and <ThemeManager /> discussed here are both fully capable to be expanded beyond light and dark themes. If your app requires a range of themes based on colours or styles, rather than light and dark, a custom <ThemeManager /> is a great solution to do so
  • It is good design language to have your primary colour be suitable for both light and dark mode, making your app distinguished in both settings. Try to find a balance in colour schemes to keep your app recognisable and brand-consistent

To delve deeper into react-navigation , check out my dedicated piece on implementing a stack navigator based on a random sample of items:

I hope these resources will aid in making your React Native apps the best they can be!

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store