DEV Community

Cover image for The Ultimate Guide to Custom Theming with React Native Paper, Expo and Expo Router
Hemanshu
Hemanshu

Posted on

The Ultimate Guide to Custom Theming with React Native Paper, Expo and Expo Router

React Native Paper is an excellent and user-friendly UI library for React Native, especially for customizing dark and light themes with its Dynamic Theme Colors Tool. However, configuring it with Expo and Expo Router can be tricky. Additionally, creating a toggle button for theme switching without a central state management system can be challenging. Expo Router can help with this.
In this article, we will learn how to:

  • Create custom light and dark themes.
  • Configure these themes for use with React Native Paper, Expo and Expo Router.
  • Implement a toggle button to switch between light and dark modes within the app.

If you like to watch this tutorial you can check out the video tutorial here:

Setup a Expo Project
Open your terminal and type:
npx create-expo-app@latest
It will ask for a project name, in my case I gave “rnpPractice”.

After Installation go into project directory and open your code editor in it and run.
Now it’s install React Native Paper and required dependency packages in the project folder.
npm install react-native-paper react-native-safe-area-context

Open babel.config.js in you code editor and change the following code:

module.exports = function(api) {
   api.cache(true);
   return {
       presets: ['babel-preset-expo'],
//ADD CODE START
       env: { 
         production: {
           plugins: ['react-native-paper/babel'],
         },
       },
//ADD CODE END
     };
  };
Enter fullscreen mode Exit fullscreen mode

Let’s reset the project so we can remove the unnecessary files, run the following to reset our project:
npm run reset-project

We will create a new folder in the root of our project called ‘ src ’ and move following folders in it:

  • app
  • components
  • constants
  • hooks It will look the following screenshot: Image description

Now in app folder let’s create a folder name (tabs), you must create a folder with parenthesis around the word tabs, that how we can create a bottom tab navigation in Expo Router. We will move our index.js file from app (the parent folder) to (tabs) (the child folder) and now create two more files in the (tabs) folder. one is “_layout.js” and 2nd settings.js. Now you app folder should look like the following screenshot:
Image description

Let’s fill settings.js with some boiler plate code:

import { StyleSheet, View, Text } from "react-native";

const Settings = () => {
  return (
    <View>
      <Text>Settings</Text>
    </View>
  );
};

const styles = StyleSheet.create({});

export default Settings;
Enter fullscreen mode Exit fullscreen mode

Now for Expo router to work properly we need to update the “_layout.js” files in app and (tabs) folder:
Update _layout.js in app folder with following code:

import {Stack} from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen
            name="(tabs)"
            options={{
              headerShown: false,
            }}
          />
    </Stack>
  )
}
Enter fullscreen mode Exit fullscreen mode

and in (tabs) folder _layout.js file:

import { Tabs } from "expo-router";
import { Feather } from "@expo/vector-icons";

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color }) => (
            <Feather name="home" size={24} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: "Setting",
          tabBarIcon: ({ color }) => (
            <Feather name="settings" size={24} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now let’s run the app and see everything is running ok.
npm run start

Now your project will start, you can run your project on emulator or simulator or on you phone by scanning the QR code and install Expo Go App on your phone.
Image description

Create and Configure the React Native Paper Theme
Let’s create our custom dark & light theme first, for that follow the link. Here you just have to select the colors Primary, Secondary and Tertiary and it will create you a light and dark theme.
Image description

Now rename your Colors.ts file to Colors.js if you are using JavaScript and not TypeScript, then open that file. Copy the light theme colors object from website and paste it in light colors object in Colors.js file and do same for the Dark theme and your file will look likes this:
Image description

export const Colors = {
  light: {
    primary: "rgb(176, 46, 0)",
    onPrimary: "rgb(255, 255, 255)",
    primaryContainer: "rgb(255, 219, 209)",
    onPrimaryContainer: "rgb(59, 9, 0)",
    secondary: "rgb(0, 99, 154)",
    onSecondary: "rgb(255, 255, 255)",
    secondaryContainer: "rgb(206, 229, 255)",
    onSecondaryContainer: "rgb(0, 29, 50)",
    tertiary: "rgb(121, 89, 0)",
    onTertiary: "rgb(255, 255, 255)",
    tertiaryContainer: "rgb(255, 223, 160)",
    onTertiaryContainer: "rgb(38, 26, 0)",
    error: "rgb(186, 26, 26)",
    onError: "rgb(255, 255, 255)",
    errorContainer: "rgb(255, 218, 214)",
    onErrorContainer: "rgb(65, 0, 2)",
    background: "rgb(255, 251, 255)",
    onBackground: "rgb(32, 26, 24)",
    surface: "rgb(255, 251, 255)",
    onSurface: "rgb(32, 26, 24)",
    surfaceVariant: "rgb(245, 222, 216)",
    onSurfaceVariant: "rgb(83, 67, 63)",
    outline: "rgb(133, 115, 110)",
    outlineVariant: "rgb(216, 194, 188)",
    shadow: "rgb(0, 0, 0)",
    scrim: "rgb(0, 0, 0)",
    inverseSurface: "rgb(54, 47, 45)",
    inverseOnSurface: "rgb(251, 238, 235)",
    inversePrimary: "rgb(255, 181, 160)",
    elevation: {
      level0: "transparent",
      level1: "rgb(251, 241, 242)",
      level2: "rgb(249, 235, 235)",
      level3: "rgb(246, 229, 227)",
      level4: "rgb(246, 226, 224)",
      level5: "rgb(244, 222, 219)",
    },
    surfaceDisabled: "rgba(32, 26, 24, 0.12)",
    onSurfaceDisabled: "rgba(32, 26, 24, 0.38)",
    backdrop: "rgba(59, 45, 41, 0.4)",
  },
  dark: {
    primary: "rgb(255, 181, 160)",
    onPrimary: "rgb(96, 21, 0)",
    primaryContainer: "rgb(135, 33, 0)",
    onPrimaryContainer: "rgb(255, 219, 209)",
    secondary: "rgb(150, 204, 255)",
    onSecondary: "rgb(0, 51, 83)",
    secondaryContainer: "rgb(0, 74, 117)",
    onSecondaryContainer: "rgb(206, 229, 255)",
    tertiary: "rgb(248, 189, 42)",
    onTertiary: "rgb(64, 45, 0)",
    tertiaryContainer: "rgb(92, 67, 0)",
    onTertiaryContainer: "rgb(255, 223, 160)",
    error: "rgb(255, 180, 171)",
    onError: "rgb(105, 0, 5)",
    errorContainer: "rgb(147, 0, 10)",
    onErrorContainer: "rgb(255, 180, 171)",
    background: "rgb(32, 26, 24)",
    onBackground: "rgb(237, 224, 221)",
    surface: "rgb(32, 26, 24)",
    onSurface: "rgb(237, 224, 221)",
    surfaceVariant: "rgb(83, 67, 63)",
    onSurfaceVariant: "rgb(216, 194, 188)",
    outline: "rgb(160, 140, 135)",
    outlineVariant: "rgb(83, 67, 63)",
    shadow: "rgb(0, 0, 0)",
    scrim: "rgb(0, 0, 0)",
    inverseSurface: "rgb(237, 224, 221)",
    inverseOnSurface: "rgb(54, 47, 45)",
    inversePrimary: "rgb(176, 46, 0)",
    elevation: {
      level0: "transparent",
      level1: "rgb(43, 34, 31)",
      level2: "rgb(50, 38, 35)",
      level3: "rgb(57, 43, 39)",
      level4: "rgb(59, 45, 40)",
      level5: "rgb(63, 48, 43)",
    },
    surfaceDisabled: "rgba(237, 224, 221, 0.12)",
    onSurfaceDisabled: "rgba(237, 224, 221, 0.38)",
    backdrop: "rgba(59, 45, 41, 0.4)",
  },
};
Enter fullscreen mode Exit fullscreen mode

Now let’s use our theme, for that open the _layout.js file in app folder and import:

import {Stack} from 'expo-router';
//Import the code Start
import {
  MD3DarkTheme,
  MD3LightTheme,
  PaperProvider,
} from "react-native-paper";
//Import the code End

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen
            name="(tabs)"
            options={{
              headerShown: false,
            }}
          />
    </Stack>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now let’s wrap our code in in _layout.js file in app folder like the following:

import {Stack} from 'expo-router';
import {
  MD3DarkTheme,
  MD3LightTheme,
  PaperProvider,
} from "react-native-paper";

export default function RootLayout() {
  return (
    <PaperProvider> //Start there
      <Stack>
        <Stack.Screen
              name="(tabs)"
              options={{
                headerShown: false,
              }}
            />
      </Stack>
    </PaperProvider> //End here
  )
}
Enter fullscreen mode Exit fullscreen mode

👆 This will allow us to use React Native Paper components in our app.

Now to apply our color theme we need to import Colors from the Colors.js file and merge it on the colors object in the current theme in React Native Paper. Confusing? 🤔
Let write the code to understand 😁. Open _layout.js file in app folder:

import {Stack} from 'expo-router';
import {
  MD3DarkTheme,
  MD3LightTheme,
  PaperProvider,
} from "react-native-paper";

//1. Import Our Colors
import { Colors } from "../constants/Colors";

//2. Overwrite it on the current theme
const customDarkTheme = { ...MD3DarkTheme, colors: Colors.dark };
const customLightTheme = { ...MD3LightTheme, colors: Colors.light };


export default function RootLayout() {
  return (
    // 3.Use any theme you like for your app
    <PaperProvider theme={customDarkTheme}> 
      <Stack>
        <Stack.Screen
              name="(tabs)"
              options={{
                headerShown: false,
              }}
            />
      </Stack>
    </PaperProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Let’s make our app decide which theme to use as per the users device. For that we are going to use a Hook provided by React Native called useColorScheme. The useColorScheme React hook provides and subscribes to color scheme preferred by the user’s device. Read More.

Open _layout.js file in app folder:

import { Stack } from 'expo-router';
//1. Import the useColorScheme hook
import { useColorScheme } from 'react-native';
import {
  MD3DarkTheme,
  MD3LightTheme,
  PaperProvider,
} from "react-native-paper";

import { Colors } from "../constants/Colors";


const customDarkTheme = { ...MD3DarkTheme, colors: Colors.dark };
const customLightTheme = { ...MD3LightTheme, colors: Colors.light };

export default function RootLayout() {
//2. Get the value in a const
const colorScheme = useColorScheme();

//3. Let's decide which theme to use
  const paperTheme =
    colorScheme === "dark" ? customDarkTheme : customLightTheme;

  return (
   //4. apply the theme
    <PaperProvider theme={paperTheme}> 
      <Stack>
        <Stack.Screen
              name="(tabs)"
              options={{
                headerShown: false,
              }}
            />
      </Stack>
    </PaperProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now it’s time to test, let’s go into (tabs) folder and open index.js and copy following code:

import { View } from "react-native";
import { Avatar, Button, Card, Text } from "react-native-paper";

const LeftContent = (props) => <Avatar.Icon {...props} icon="folder" />;

export default function Index() {
  return (
    <View
      style={{
        flex: 1,
        margin: 16,
      }}
    >
      <Card>
        <Card.Cover source={{ uri: "https://picsum.photos/700" }} />
        <Card.Title
          title="Card Title"
          subtitle="Card Subtitle"
          left={LeftContent}
        />
        <Card.Content>
          <Text variant="bodyMedium">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam
            tenetur odit eveniet inventore magnam officia quia nemo porro?
            Dolore sapiente quos illo distinctio nisi incidunt? Eaque officiis
            iusto exercitationem natus?
          </Text>
        </Card.Content>
        <Card.Actions>
          <Button>Open</Button>
        </Card.Actions>
      </Card>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

You can see that your theme is applied but only on the card you imported from React Native Paper, your navigation is still using a default theme provided by Expo Router.

Now let’s merge the Expo Router theme in React Native Paper theme and use the same theme for both. To achieve that let’s get back to _layout.js file in app folder and make the following changes:

import { Stack } from 'expo-router';
import { useColorScheme } from 'react-native';
import {
  MD3DarkTheme,
  MD3LightTheme,
  PaperProvider,
  adaptNavigationTheme, //1. Import this package
} from "react-native-paper";

//2. Import Router Theme
import {
  DarkTheme as NavigationDarkTheme,
  DefaultTheme as NavigationDefaultTheme,
  ThemeProvider,
} from "@react-navigation/native";

//3. Install deepmerge first and import it
import merge from "deepmerge";

import { Colors } from "../constants/Colors";

const customDarkTheme = { ...MD3DarkTheme, colors: Colors.dark };
const customLightTheme = { ...MD3LightTheme, colors: Colors.light };

//4. The adaptNavigationTheme function takes an existing React Navigation 
// theme and returns a React Navigation theme using the colors from 
// Material Design 3.
const { LightTheme, DarkTheme } = adaptNavigationTheme({
  reactNavigationLight: NavigationDefaultTheme,
  reactNavigationDark: NavigationDarkTheme,
});

//5.We will merge React Native Paper Theme and Expo Router Theme 
// using deepmerge
const CombinedLightTheme = merge(LightTheme, customLightTheme);
const CombinedDarkTheme = merge(DarkTheme, customDarkTheme);

export default function RootLayout() {
    const colorScheme = useColorScheme();

  //6. Let's use the merged themes
    const paperTheme =
      colorScheme === "dark" ? CombinedDarkTheme : CombinedLightTheme;

    return (
      <PaperProvider theme={paperTheme}>
        //7.We need to use theme provider from react navigation 
        //to apply our theme on Navigation components
        <ThemeProvider value={paperTheme}>
          <Stack>
            <Stack.Screen
                  name="(tabs)"
                  options={{
                    headerShown: false,
                  }}
                />
          </Stack>
        </ThemeProvider>
      </PaperProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

That's it guys this should apply our theme to both React Native Paper Components and Navigation Components like Header Navigation or Bottom Tag Navigation.

Find the source code here:
https://github.com/hemanshum/React-Native-Paper-Practic-App

Wrapping Up
We’ve covered a lot of ground in this blog post, from creating custom light and dark themes to configuring them for use with React Native Paper, Expo and Expo Router. By now, you should have a solid foundation for implementing theming in your Expo projects. For those looking to add a toggle button to switch between these themes within your app, I’ve created a detailed video tutorial. Check out the video, with a convenient timestamp for the relevant section, here: Video Tutorial.

Happy coding, and may your apps always look great — in light and in dark! 🌓✨

Top comments (0)