React Native Dark Mode and Theming

Alex Borton
Nov 1 · 5 min read

The new Dark mode in iOS got me all excited. I could use all my favourite apps but now they were Dark 😎. Ok, not that exciting but I think for a lot of people, just a change in the presentation is refreshing.

I had to immediately make a few amendments to our application at work, and I also got thinking about how I could roll this feature out to my own applications. Naturally, they both follow a similar pattern when it comes to colours (or colors 🙄) and theming.

I am going to talk through the approach and setup I have come up with to theming and Dark mode and how this could be adapted to make it even more versatile for users to customise their experience in your app.

Current setup

Currently, we centralise our colours and theme in one place. For the sake of this article, I will only talk about colours, but of course theming could and probably should contain spacing, font, icon… whatever you want to persist across your application with a certain amount of consistency.

/src
-- /theme/index.js

Right at the root, we have our theme . This is where we store our colours (and everything else that you want to use app-wide). Here is an example of what that looks like;

// theme/index.jsconst palette = {
palette01: '#000000',
palette02: 'rgba(255,255,255,1)',
}
export const colors = {
paragraphText: palette.palette01,
buttonPrimaryBg: palette.palette02,
headingText: palette.palette01,
}

Why split the palette and the colours? Great question. Well, this way we can define the colours on a more granular level by their use case and reuse from our palette. This makes sure that we aren’t coming up with 50 shades of grey 😏. Sure, we will end up repeating the palette usage throughout the colours, but that doesn’t actually matter and it’s kind of the point. It also means that when it comes to theming, we can either change the palette or the colours individually (or both, if we wanted).

Just a quick example of how they are used for clarity;

// ButtonPrimary/index.js...import { colors } from 'theme'const ButtonPrimary = ({ onPress, children }) => (
<TouchableOpacity onPress={onPress} style={styles}>
{children}
</TouchableOpacity>
)
const styles = {
backgroundColor: colors.buttonPrimaryBg,
}
export default ButtonPrimary

Nothing exciting going on here, but we do have our theme essentially decoupled from our components themselves. Great start.

Dark mode

Dark mode is the subject of this short article and is exclusive to iOS, but there are also other theme/UI influencing setups out there for Android like Night Mode. I imagine this will become more common in the future and regardless of a device preference, this same idea could be used to change a users internal settings.

If you are using Expo, like I am (I really love it… but more on that another time) then you can use react-native-appearance to listen to the users device preferences. This is currently only for iOS and doesn’t take into account Android, but as per the docs here;

The Appearance API will likely be available in react-native@>=0.61

So this should set us up for future use. There are also other modules if you are not using Expo, like react-native-dark-mode that take a similar approach.

react-native-appearance gives us a couple of ways to work with the users preferences, but the most straight forward and practical to me seems to be the hooks.

I am not going to get into how to use hooks, but I recommend you take some time to read about them. They are awesome.

Here is the react-native-appearance hook in use, straight out the docs;

...import { useColorScheme } from 'react-native-appearance'function MyComponent() {  let colorScheme = useColorScheme()  if (colorScheme === 'dark') {   // render some dark thing  } else {  // render some light thing  }}

So if we take our earlier example of a ButtonPrimary , we could do something like this;

// ButtonPrimary/index.jsimport { useColorScheme } from 'react-native-appearance'
import { colors } from 'theme'
const ButtonPrimary = ({ onPress, children }) => {
const colorScheme = useColorScheme()
return (
<TouchableOpacity
onPress={onPress}
style={{
backgroundColor: colorScheme === 'dark'
? colors.buttonPrimaryBgDark
: colors.buttonPrimaryBg
}}
>
{children}
</TouchableOpacity>
)
export default ButtonPrimary

I mean… it would work. But it’s not very readable and all that logic around selecting a colour is now in our component and every other component we might want to influence, which with a theme could be most presentational components.

So it’s not practical. But it is easily fixed.

I will create my own hook that will use the useColorScheme hook and as well as returning my themed colours. It can also return the colour scheme, should I need it. This will mean a couple of changes to my colors object that we wrote earlier, but nothing too drastic. Looking at that first;

// theme/index.jsconst palette = {
palette01: '#000000',
palette02: 'rgba(255,255,255,1)',
}
export const colors = {
paragraphText: palette.palette01,
buttonPrimaryBg: palette.palette02,
headingText: palette.palette01,
}
export const themedColors = {
default: {
...colors,
},
light: {
...colors,
},
dark: {
...colors,
buttonPrimaryBg: palette.palette01,
paragraphText: palette.palette02,
},
}

Again, nothing too exciting going on here — just the addition of a new object themedColors this is where we will do the theming. light and default are doing the same thing at the moment, simply spreading in the original colour set, but dark is now having some of those original colours overwritten with a theme specific set (notice how the colours are now inverted for dark vs light .

Now for the hook.

// theme/hooks.jsimport { useColorScheme } from 'react-native-appearance'import { themedColors } from '.'export const useTheme = () => {
const theme = useColorScheme()
const colors = theme ? themedColors[theme] : themedColors.default return {
colors,
theme,
}
}

We return the colours and the theme from our custom hook. The colors will already be assigned to the users preferred theme (if it’s available), falling back to the default if nothing is selected or defined.

Usage of this hook becomes strikingly similar to our original implementation of the ButtonPrimary .

// ButtonPrimary/index.js...import { useTheme } from 'theme/hooks'const ButtonPrimary = ({ onPress, children }) => {
const { colors } = useTheme()
return (
<TouchableOpacity
onPress={onPress}
style={{
backgroundColor: colors.buttonPrimaryBg
}}
>
{children}
</TouchableOpacity>
)
}
export default ButtonPrimary

All we need to do is import the custom hook and deconstruct the colours from there, instead of directly from our theme 🎉

This is scalable and manageable for a big roll out across the whole application.

The downside

There is always one… we have to write our colour style inline, which is a bit of a bummer. You can still spread in the rest of your styles too if you need to, but it does make it a tiny bit messier. It seems to me that it’s an OK trade off for themed styles throughout.

Expanding the example

This just shows how to use the users device settings and it’s only limited to iOS. You could expand the hook to include settings from the user with relative ease, meaning a user could decide on their in-app settings to override the device settings.

JavaScript in Plain English

Learn the web's most important programming language.

Alex Borton

Written by

React and React Native developer. Making apps and websites and that... Keen surfer and Leather enthusiast.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade