Introduction
In this post we will be implementing light and dark mode in our React-Native app using styled-components, context, and react-native-appearance. By the end of the post our app will default to the OS theme on start, update on OS theme change, and toggle light and dark based off of the switch. If a picture says a thousand words than a GIF says like a million or something so just look at this GIF of what we will be making.
Initialize Project
npx react-native init lightSwitch --template react-native-template-typescript cd lightSwitch
git init git add -A git commit -m "react-native init"
Add Styled-Components
yarn add styled-components yarn add --dev @types/styled-components
Theming
Handle System Mode
Currently React-Native doesn’t have an API for checking if the device is set to dark mode so we need this library.
yarn add react-native-appearance
iOS
cd ios/ pod install cd ..
Android
Add the uiMode flag
// android/app/src/main/AndroidManifest.xml <activity ... android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"> ... >
Implement the onConfigurationChanged method
// android/app/src/main/java/com/<PROJECT_NAME>/MainActivity.java import android.content.Intent; // <--- import import android.content.res.Configuration; // <--- import public class MainActivity extends ReactActivity { ...... // copy these lines @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); Intent intent = new Intent("onConfigurationChanged"); intent.putExtra("newConfig", newConfig); sendBroadcast(intent); } ...... }
Define Types
// types.ts export type ThemeMode = 'light' | 'dark'; export interface ThemeContext { mode: ThemeMode; setMode(mode: ThemeMode): void; } export interface Theme { theme: { background: string; border: string; backgroundAlt: string; borderAlt: string; text: string; }; }
Add Themes
Light
// themes/light.ts import {Theme} from '../types'; const light: Theme = { theme: { background: '#ededed', border: '#bdbdbd', backgroundAlt: '#eaeaeb', borderAlt: '#bdbdbd', text: '#171717', }, }; export default light;
Dark
// themes/dark.ts import {Theme} from '../types'; const dark: Theme = { theme: { background: '#2E3440', border: '#575c66', backgroundAlt: '#575c66', borderAlt: '#2E3440', text: '#ECEFF4', }, }; export default dark;
Create Context
The ManageThemeContext is where all the magic happens. Here we create a new context, get the current OS mode, pass the theme into the styled-component’s ThemeProvider, register for OS mode changes, and set the status bar color appropriately.
// contexts/ManageThemeContext.tsx import React, {createContext, useState, FC, useEffect} from 'react'; import {ThemeMode, ThemeContext} from '../types'; import {StatusBar} from 'react-native'; import {ThemeProvider} from 'styled-components/native'; // @ts-ignore import {Appearance, AppearanceProvider} from 'react-native-appearance'; import lightTheme from '../themes/light'; import darkTheme from '../themes/dark'; // Get OS default mode or default to 'light' const defaultMode = Appearance.getColorScheme() || 'light'; // Create ManageThemeContext which will hold the current mode and a function to change it const ManageThemeContext = createContext<ThemeContext>({ mode: defaultMode, setMode: mode => console.log(mode), }); // Export a helper function to easily use the Context export const useTheme = () => React.useContext(ManageThemeContext); // Create the Provider const ManageThemeProvider: FC = ({children}) => { const [themeState, setThemeState] = useState(defaultMode); const setMode = (mode: ThemeMode) => { setThemeState(mode); }; // Subscribe to OS mode changes useEffect(() => { const subscription = Appearance.addChangeListener( ({colorScheme}: {colorScheme: ThemeMode}) => { setThemeState(colorScheme); }, ); return () => subscription.remove(); }, []); // Return a component which wraps its children in a styled-component ThemeProvider, // sets the status bar color, and injects the current mode and a function to change it return ( <ManageThemeContext.Provider value={{mode: themeState as ThemeMode, setMode}}> <ThemeProvider theme={themeState === 'dark' ? darkTheme.theme : lightTheme.theme}> <> <StatusBar barStyle={themeState === 'dark' ? 'light-content' : 'dark-content'} /> {children} </> </ThemeProvider> </ManageThemeContext.Provider> ); }; // This wrapper is needed to add the ability to subscribe to OS mode changes const ManageThemeProviderWrapper: FC = ({children}) => ( <AppearanceProvider> <ManageThemeProvider>{children}</ManageThemeProvider> </AppearanceProvider> ); export default ManageThemeProviderWrapper;
Example Usage
This is the example code behind the GIF at the top of this post.
import React from 'react'; import {Theme} from './types'; import styled from 'styled-components/native'; import ThemeManager, { useTheme } from './contexts/ManageThemeContext'; import { Switch } from 'react-native'; const Home = () => { // Helper function => useContext(ManageThemeContext) const theme = useTheme(); return ( <Container> <Switch value={theme.mode === 'dark'} onValueChange={value => theme.setMode(value ? 'dark' : 'light')} /> </Container> ); }; // Get the background color from the theme object const Container = styled.View<Theme>` flex: 1; justify-content: center; align-items: center; background: ${props => props.theme.background}; `; // Wrap Home in the ThemeManager so it can access the current theme and // the function to update it const App = () => ( <ThemeManager> <Home /> </ThemeManager> ); export default App;
Conclusion
Adding a light and dark mode to your app is pretty simple and adds a cool dynamic. It is also the hot, new thing to do so get to it!
Top comments (0)