loading...

React-Native Dark Mode

thefinnomenon profile image Chris Finn Originally published at thefinnternet.com on ・3 min read

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.

Project Demo

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!

Checkout This Project’s Code On Github

Posted on by:

thefinnomenon profile

Chris Finn

@thefinnomenon

Software engineer from Boston, MA. Love all things React & also pizza 🍕.

Discussion

markdown guide