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
};
};
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:
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:
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;
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>
)
}
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>
);
}
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.
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.
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:
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)",
},
};
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>
)
}
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
)
}
👆 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>
)
}
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>
)
}
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>
);
}
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>
)
}
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)