Adding Dark Mode to my Expo based React Native app
— JavaScript, Software Development — 4 min read
Last week I added a new date picker component to my React Native app — JiffyCV. This date picker looked amazing in my component library and I was really proud of how it was working.
Then, I updated my app pull in the new version of the component library, added the new component, took it for a test run on my iPhone and was presented with a blank box where the date picker was meant to be.
After a couple of new releases of changing the code in a desperate bid to get it to work and failing I finally found the answer to my problems — The date picker was there, it’s just that as my phone was in dark mode the text was white and my picker just happened to be on a white background too.
I felt like a complete idiot, having wasted time thinking it was a code fix but then I started to think — If my phone is on dark mode then how come my component library didn’t have the same issue? It turns out that storybook was setting the theme to light mode regardless of the phone’s setting.
I then came to realise that if I was having these issues then it’s likely that others will have similar issues and I should probably look to support both dark and light modes.
Designing for Dark Mode
When designing the app I had used Figma to build prototypes in order to carry out some user testing and validate the idea before writing any code.
As I had all the screens for the app already laid out from my initial designs I was able to use Figma’s palette swapping functionality to create copies of the designs and change the colours used for the dark mode colours.
This made things really easy to work with and sped up iterating over designs massively, and there was many iterations because it turns out designing for Dark Mode isn’t that easy.
Dark Mode isn’t just a case of inverting the light mode palette, you still need to convey the correct hierarchy of information and a simple white -> black inversion would flip this hierarchy on it’s head.
Additionally, if you have bright colours you may find they become incredibly jarring when used in the Dark Mode context, I had to create a set of muted tones for my accent colour as the brightness was just too much in Dark Mode.
Compare the three images below; the left is just an inversion of light mode, the middle is with making sure that the hierarchy of the screen elements is maintained and the right is the light mode version.
In order to create a sense of depth in a design that’s already incredibly dark I used ‘proper’ black (#000000) for the darkest colour instead of the slightly lighter shade I had been using in light mode.
This allowed me use that slightly lighter shade as the base colour for the rest of the design (essentially becoming the dark mode equivalent of white in my designs).
Another important factor is contrast, because of the shift in background colour you may find that any accent colours that were compliant with WCAG are no longer contrasted enough to pass. This combined with the jarring effect mentioned earlier means it can be hard to adapt designs to dark mode.
Bringing Dark Mode to your Expo based React Native app
Expo makes it pretty simple to handle how your app responds to the user’s colour settings within it’s app.json
file, using the userInterfaceStyle
property which can be set to automatic
to honour the user’s current colour scheme choice or light
and dark
to force the app into one of the colour schemes.
You can also set the userInterfaceStyle
property inside the ios
and android
objects to manage these at a platform level. For my app I opted for automatic
across both.
To detect the colour scheme within your app you’ll need to add the react-native-appearance
package to your project, as this gives us the AppearanceProvider
context we’ll wrap our app in and the useColorScheme
hook we’ll use at the component level to ensure the correct stylesheet is used.
Here’s an example of using the AppearanceProvider
to wrap the app.
Here’s an example of using theuseColorScheme
hook in a component, the hook returns 'dark'
if the user is using dark mode, so a check for this can be used to load in a dark mode style sheet.
In order to have both light and dark mode styles available in my components I took the existing stylesheet used in light mode and extracted the style object into a const I called baseStyles
.
I then created a darkModeOverrides
const that copied and amended the baseStyles
before creating stylesheet instances for light and dark modes based off the respective objects.
I then used the output of useColorScheme
to use the correct stylesheet within my components.
This approach allowed me to quickly implement dark mode by only focusing on the things I needed to change, but if I was to refactor I’d most likely remove the structural styles from the colour ones to make it even cleaner.
In order to test my UI under light and dark mode I created a simple mock for react-native-appearance
that allows me to return ‘dark’ when I need to test the UI in dark mode.
Gotcha — React Native Storybook
I built my component library using the React Native version of Storybook that Expo provides in one of their starter projects.
I ended up having to upgrade the version of @storybook/.react-native
to 5.3.23
in order to have the userInterfaceStyle
setting honoured by storybook.
I also had to make changes to the App.tsx
file in order to wrap the main Storybook view in the AppearanceProvider
. A gist of how that looks can be found below.
It’s worth noting that while you can provide a theme to Storybook it will only style it’s elements. The base view of the application still has a white background but these changes were enough at least for me to test my components while creating them.
Summary
Unless you’re using components that aren’t colour scheme aware you can probably build an app without having to concern yourself with building an adaptive UI.
However, if like me you use a native date picker on iOS you may find yourself with a UI that doesn’t work unless you decide to force a particular mode or build something a little more adaptive.
If you do decide to build an adaptive UI then it’s important to understand and test how your UI will work in dark mode instead of just inverting your existing colour scheme.
Once you’re happy with how your UI will work then react-native-appearance
will help you detect the colour scheme the user is using and allow you to build adaptive UI components.