Crafting Dynamic UIs with Custom Themes in React Native

Cover Image for Crafting Dynamic UIs with Custom Themes in React Native
Rajeshkumar S
Rajeshkumar S
6 days ago
webappmobiledevelopmentandroidJavaScriptReactReact NativeiOScross-platform

Introduction

In the vibrant world of mobile app development, creating visually appealing and user-friendly interfaces is paramount. React Native, a powerful framework for building cross-platform mobile applications, offers developers a plethora of tools and techniques to achieve this goal. One such technique that significantly enhances the user experience is implementing custom themes.

Themes not only dictate the visual appearance of your application but also contribute to its overall aesthetic and brand identity. In this comprehensive guide, we'll delve into the intricacies of implementing custom themes in React Native, empowering you to craft dynamic and immersive UIs for your mobile apps.

Understanding the Fundamentals

Before diving into the implementation details, let's grasp the fundamentals of custom themes in React Native. At its core, a theme encompasses a set of stylistic attributes such as colors, typography, and spacing that define the look and feel of an application. By utilizing themes, developers can seamlessly switch between different visual styles and accommodate user preferences, creating a personalized experience for each user.

Building a Theme Provider

The cornerstone of our custom theme implementation lies in the ThemeProvider component. This component serves as the central hub for managing theme-related logic and providing theme context to other components within the application. Here's a breakdown of its key functionalities:

  1. Dynamic Theme Detection: Leveraging the Appearance module, our ThemeProvider detects the device's color scheme (light or dark) and initializes the theme accordingly. Additionally, it provides an option to override this detection with a custom theme.
  2. Theme Switching: The toggleTheme function enables users to seamlessly switch between light and dark themes, enhancing accessibility and catering to individual preferences.
  3. Theme Configuration: Developers can customize various aspects of the theme such as colors, typography, and spacing by providing a themeConfig object to the ThemeProvider.

Building a Foundation: Folder Structure

Before we delve into the intricacies of custom themes in React Native, let's establish a solid foundation by organizing our project structure. A well-structured folder hierarchy enhances modularity, maintainability, and scalability, ensuring that our custom theme implementation remains robust and flexible.

Folder Structure

DynamicUI
    node_modules
    src
      components
      routes
      screens
      service
      utils
        theme
          color
            index.ts
            types.d.ts
          index.ts
          types.d.ts
        localstorage.ts
    App.tsx
    package.json
    tsconfig.json

Within our project's directory structure lies a hidden gem - the utils/theme directory. This enclave houses essential utilities that power our application's theming capabilities. Let's embark on a journey to unravel its secrets and understand how it contributes to our dynamic UI experience.

Understanding the Theme Structure

At the heart of the theme directory lies the color subdirectory. Here, we define color-related configurations for different themes, laying the foundation for our application's visual identity.

  • color/: This directory contains color definitions and configurations for various themes, allowing us to maintain consistency and coherence in our UI design.

Key Files and Definitions

As we delve deeper, we encounter two pivotal files that shape our theme utilities:

  • index.ts: This file serves as the nexus of our theme utilities. It exports theme-related constants, configurations, and functions, enabling seamless integration of themes throughout our application.
  • types.d.ts: TypeScript declaration files provide type definitions for theme-related objects. These definitions ensure type safety and enhance developer productivity by providing clear and concise interfaces for working with themes.

The utils/theme directory serves as a cornerstone of our application's theming infrastructure. Through meticulous organization and thoughtful design, it empowers us to create dynamic and immersive UI experiences that resonate with our users. As we continue to explore the depths of our project's directory structure, let's cherish the ingenuity and craftsmanship that underpin our theme utilities.

Unraveling the Components: Code Breakdown

  1. src/utils/theme/index.tsx
index.ts
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Appearance, type ColorSchemeName } from 'react-native';
import type { ThemeContextProps, ThemeProviderProps } from './types';
import { CUSTOM_THEME, DARK_THEME, LIGHT_THEME } from './color';
import type { Theme } from './color/types';
 
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
 
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
  children,
  themeConfig = {},
  useTheme = 'light',
}) => {
  const [theme, setTheme] = useState<ColorSchemeName | 'custom'>(
    useTheme || Appearance.getColorScheme() || 'light'
  );
 
  useEffect(() => {
    const subscription = Appearance.addChangeListener(({ colorScheme }) => {
      setTheme(colorScheme || 'light');
    });
 
    return () => {
      subscription.remove();
    };
  }, []);
 
  const toggleTheme = () => {
    setTheme((prevTheme) => {
      if (prevTheme === 'light') {
        return 'dark';
      } else if (prevTheme === 'dark') {
        return 'light';
      } else {
        return 'light';
      }
    });
  };
 
  const getColorsForTheme = () => {
    const themeColors =
      theme === 'light'
        ? {
            ...LIGHT_THEME,
            ...themeConfig.light,
          }
        : theme === 'dark'
          ? {
              ...DARK_THEME,
              ...themeConfig.dark,
            }
          : {
              ...CUSTOM_THEME,
              ...themeConfig.custom,
            };
 
    return themeColors;
  };
 
  const colors: Theme = getColorsForTheme();
 
  return (
    <ThemeContext.Provider
      value={
        {
          theme: theme as ColorSchemeName,
          toggleTheme,
          colors,
        } as ThemeContextProps
      }
    >
      {children}
    </ThemeContext.Provider>
  );
};
 
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

This file serves as the nexus of our theme utilities. It exports theme-related constants, configurations, and functions, enabling seamless integration of themes throughout our application

  1. src/utils/theme/types.d.ts
types.d.ts
import { ColorSchemeName, Record } from "react-native";
import { Theme } from "./color/types";
 
export type CustomColors = {
  light?: Theme,
  dark?: Theme,
  custom?: Theme,
};
 
export type ThemeContextProps = {
  theme: ColorSchemeName,
  toggleTheme: () => void,
  colors: Theme,
};
 
export type ThemeProviderProps = {
  children: React.ReactNode,
  themeConfig?: CustomColors,
  useTheme?: ColorSchemeName | "custom",
  customColors?: CustomColors,
};

TypeScript declaration files provide type definitions for theme-related objects. These definitions ensure type safety and enhance developer productivity by providing clear and concise interfaces for working with themes.

  1. src/utils/theme/color/index.ts
index.ts
import type { Theme } from "./types";
 
const COMMON_THEME = {
  PRIMARY_FOREGROUND: "hsl(0, 0%, 98%)",
  SECONDARY_FOREGROUND: "hsl(240, 5.9%, 10%)",
  ACCENT_FOREGROUND: "hsl(0, 0%, 98%)",
  DESTRUCTIVE_FOREGROUND: "hsl(0, 0%, 98%)",
  BORDER: "hsl(240, 5.9%, 90%)",
  INPUT: "hsl(240, 5.9%, 90%)",
};
 
const LIGHT_THEME: Theme = {
  ...COMMON_THEME,
  BACKGROUND: "hsl(0, 0%, 100%)",
  FOREGROUND: "hsl(240, 10%, 3.9%)",
  CARD: "hsl(0, 0%, 100%)",
  CARD_FOREGROUND: "hsl(240, 10%, 3.9%)",
  POPOVER: "hsl(0, 0%, 100%)",
  MUTED_FOREGROUND: "hsl(240, 6%, 10.9%)",
  POPOVER_FOREGROUND: "hsl(240, 10%, 3.9%)",
  PRIMARY: "hsl(262.1 83.3% 57.8%)",
  SECONDARY: "hsl(240, 4.8%, 95.9%)",
  MUTED: "hsl(240, 6.8%, 95.9%)",
  ACCENT: "hsl(240, 4.8%, 95.9%)",
  DESTRUCTIVE: "hsl(0, 84.2%, 60.2%)",
  RING: "hsl(240, 4.9%, 83.9%)",
};
 
const DARK_THEME: Theme = {
  ...COMMON_THEME,
  BACKGROUND: "hsl(240, 10%, 3.9%)",
  FOREGROUND: "hsl(0, 0%, 98%)",
  CARD: "hsl(240, 10%, 3.9%)",
  CARD_FOREGROUND: "hsl(0, 0%, 98%)",
  POPOVER: "hsl(240, 10%, 3.9%)",
  MUTED_FOREGROUND: "hsl(240, 5%, 64.9%)",
  POPOVER_FOREGROUND: "hsl(0, 0%, 98%)",
  PRIMARY: "hsl(262.1 83.3% 57.8%)",
  SECONDARY: "hsl(240, 3.7%, 15.9%)",
  MUTED: "hsl(240, 3.7%, 15.9%)",
  ACCENT: "hsl(240, 3.7%, 15.9%)",
  DESTRUCTIVE: "hsl(0, 62.8%, 30.6%)",
  RING: "hsl(240, 4.9%, 83.9%)",
};
 
const CUSTOM_THEME = {
  BACKGROUND: "hsl(222.2, 84%, 4.9%)",
  FOREGROUND: "hsl(210, 40%, 98%)",
  CARD: "hsl(222.2, 84%, 4.9%)",
  CARD_FOREGROUND: "hsl(210, 40%, 98%)",
  POPOVER: "hsl(222.2, 84%, 4.9%)",
  POPOVER_FOREGROUND: "hsl(210, 40%, 98%)",
  PRIMARY: "hsl(217.2, 91.2%, 59.8%)",
  PRIMARY_FOREGROUND: "hsl(222.2, 47.4%, 11.2%)",
  SECONDARY: "hsl(217.2, 32.6%, 17.5%)",
  SECONDARY_FOREGROUND: "hsl(210, 40%, 98%)",
  MUTED: "hsl(217.2, 32.6%, 17.5%)",
  MUTED_FOREGROUND: "hsl(215, 20.2%, 65.1%)",
  ACCENT: "hsl(217.2, 32.6%, 17.5%)",
  ACCENT_FOREGROUND: "hsl(210, 40%, 98%)",
  DESTRUCTIVE: "hsl(0, 62.8%, 30.6%)",
  DESTRUCTIVE_FOREGROUND: "hsl(210, 40%, 98%)",
  BORDER: "hsl(217.2, 32.6%, 17.5%)",
  INPUT: "hsl(217.2, 32.6%, 17.5%)",
  RING: "hsl(224.3, 76.3%, 48%)",
};
 
export { LIGHT_THEME, DARK_THEME, CUSTOM_THEME };

This file serves as the central hub for theme-related configurations and constants.

  • It exports the LIGHT_THEME, DARK_THEME, and CUSTOM_THEME objects, making theme definitions accessible throughout the application.
  • LIGHT_THEME and DARK_THEME define color palettes and configurations for light and dark themes, respectively.
  • CUSTOM_THEME provides customization options for users to create personalized themes with unique color schemes.
  1. src/utils/theme/color/types.d.ts
types.d.ts
export type Theme = {
  PRIMARY_FOREGROUND: string,
  SECONDARY_FOREGROUND: string,
  MUTED_FOREGROUND: string,
  ACCENT_FOREGROUND: string,
  DESTRUCTIVE_FOREGROUND: string,
  BORDER: string,
  INPUT: string,
  BACKGROUND: string,
  FOREGROUND: string,
  CARD: string,
  CARD_FOREGROUND: string,
  POPOVER: string,
  POPOVER_FOREGROUND: string,
  PRIMARY: string,
  SECONDARY: string,
  MUTED: string,
  ACCENT: string,
  DESTRUCTIVE: string,
  RING: string,
};

TypeScript declaration file providing type definitions for theme-related objects.

  • It ensures type safety and consistency when working with theme configurations and colors throughout the application.

  • Defines the Theme type, specifying the structure of theme objects, including properties such as BACKGROUND, FOREGROUND, PRIMARY, SECONDARY, etc.

    💡

    NOTE: The theme variables can be customized based on your application's design requirements and brand identity. You can provide any name to the theme variables and define the corresponding color values to match your desired color scheme.

How to use the ThemeProvider 🤔

How to use the ThemeProvider in your React Native application to enable dynamic theming and customization capabilities first import the ThemeProvider from the theme utilities and wrap your application's root component with it.

App.tsx
import { NavigationContainer } from "@react-navigation/native";
import AppRout from "@routes/AppRoute";
import { UserProvider } from "@utils/UserContext";
import { ThemeProvider } from "@utils/theme";
import { useEffect, useState } from "react";
import { useColorScheme } from "react-native";
 
const App = () => {
  const deviceColorScheme = useColorScheme();
  const [currentTheme, setCurrentTheme] = useState(
    deviceColorScheme || "light"
  );
 
  useEffect(() => {
    console.log("Device color scheme:", deviceColorScheme);
    setCurrentTheme(deviceColorScheme || "light");
  }, [deviceColorScheme]);
 
  return (
    <UserProvider>
      <NavigationContainer>
        <ThemeProvider useTheme={currentTheme}>
          <AppRout />
        </ThemeProvider>
      </NavigationContainer>
    </UserProvider>
  );
};
 
export default App;

In this example, we import the ThemeProvider from the theme utilities and wrap our application's root component with it.

  • We use the useColorScheme hook from react-native to detect the device's color scheme (light, dark or custom).
  • The currentTheme state variable stores the current theme value, which is initialized with the device's color scheme.
  • We update the currentTheme state whenever the device's color scheme changes, ensuring that our application's theme remains in sync with the device settings.

You can use the colors by using the following method:

Import the colors from useTheme and use the dynamic colors in your application.

eg.

Button.tsx
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "@utils/theme";
 
const Button = ({ title, onPress }) => {
  const { colors } = useTheme();
 
  return (
    <TouchableOpacity
      style={{
        backgroundColor: colors.PRIMARY,
        padding: 16,
        borderRadius: 8,
      }}
      onPress={onPress}
    >
      <Text style={{ color: colors.PRIMARY_FOREGROUND }}>{title}</Text>
    </TouchableOpacity>
  );
};
 
export default Button;

If you want to toggele the theme in your application, you can use the toggleTheme function from the useTheme hook.

we have used the dynamic import (alis path) to import the theme utilities in our application. This allows us to maintain a clean and concise import structure, enhancing code readability and maintainability. If you want to learn more about setting up global paths in your React Native project, check out our detailed guide on Setting up Global Paths in React Native .

Conclusion

In conclusion, custom themes play a pivotal role in shaping the visual identity and user experience of React Native applications. By leveraging the ThemeProvider component, developers can seamlessly integrate dynamic theme switching and customization capabilities into their projects, thereby enhancing accessibility, brand recognition, and user satisfaction. With careful planning and attention to detail, you can unlock the full potential of custom themes and elevate your mobile app development endeavors to new heights.

If you want to explore more about custom themes in React Native, subscribe to our newsletter and stay updated with the latest trends and techniques in mobile app development. Happy theming! 🥳