UTA DevHub
UI Development/Theme Management

Theme Implementation

Practical guide for implementing the theme system in components, including provider setup and hooks.

Theme Implementation Guide

This guide provides detailed implementation guidance for the application's theme system. It covers setup, management, and best practices for theme implementation.

Theming Quickstart

Need to get up and running quickly? Here's the minimal code you need to implement theming.

// 1. Import the ThemeProvider and default theme
import { ThemeProvider, defaultTheme } from '@core/shared/styles';
 
// 2. Wrap your app with ThemeProvider
function App() {
  return (
    <ThemeProvider initialTheme={defaultTheme}>
      <YourAppContent />
    </ThemeProvider>
  );
}
 
// 3. Use the theme in your components
import { useTheme } from '@core/shared/styles';
 
function MyComponent() {
  const theme = useTheme();
  
  return (
    <View style={{ 
      backgroundColor: theme.colors.background,
      padding: theme.spacing.m
    }}>
      <Text style={{ color: theme.colors.text }}>
        Themed Component
      </Text>
    </View>
  );
}

Overview

This guide provides practical implementation details for the theme system in our React Native application. It covers theme definition, provider setup, typings, and component integration patterns. The focus is on code examples and best practices for day-to-day theme usage.

Quick Start

Add the ThemeProvider to your application root:

// App.tsx
import React from 'react';
import { ThemeProvider } from '@/core/shared/styles/ThemeProvider';
 
export default function App() {
  return (
    <ThemeProvider>
      {/* App content */}
    </ThemeProvider>
  );
}

Purpose & Scope

This document provides implementation guidance for:

  • Setting up the theme context and provider
  • Creating and organizing theme files
  • Using themes in React Native components
  • Creating responsive and adaptive theming
  • Performance optimization techniques

This guide is intended for developers who need to work with the theme system in day-to-day development.

Platform-Specific Theme Customization

Mobile-Specific Theming

React Native applications often need platform-specific styling adjustments. Here's how to implement effective mobile theme customization:

// In your theme configuration
export const theme = {
  // Base theme properties
  ...baseTheme,
  // Platform-specific overrides
  ...Platform.select({
    ios: {
      // iOS-specific theme values
      spacing: {
        xs: 4,
        s: 8,
        m: 16,
        l: 24,
        xl: 32,
      },
      // iOS-specific shadows
      shadows: {
        small: {
          shadowColor: '#000',
          shadowOffset: { width: 0, height: 1 },
          shadowOpacity: 0.2,
          shadowRadius: 1.41,
        },
        // Other shadow sizes...
      },
    },
    android: {
      // Android-specific theme values
      spacing: {
        xs: 4,
        s: 8,
        m: 16,
        l: 20,
        xl: 28,
      },
      // Android uses elevation instead of shadow properties
      shadows: {
        small: {
          elevation: 2,
        },
        // Other shadow sizes...
      },
    },
  }),
};

For better maintainability, use a dedicated helper for platform-specific values:

const getPlatformValue = (ios: any, android: any) => Platform.OS === 'ios' ? ios : android;
 
// Example usage
const shadowStyle = getPlatformValue(
  { shadowColor: '#000', shadowOffset: {width: 0, height: 2}, shadowOpacity: 0.2, shadowRadius: 3 },
  { elevation: 4 }
);

Safe Area Handling

Mobile devices have notches, home indicators and other UI elements that require content padding. Use the theme system to handle these consistently:

import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
function ThemedScreen() {
  const theme = useTheme();
  const insets = useSafeAreaInsets();
  
  return (
    <View style={{
      flex: 1,
      backgroundColor: theme.colors.background,
      // Apply safe area padding using theme spacing
      paddingTop: insets.top + theme.spacing.m,
      paddingBottom: insets.bottom + theme.spacing.m,
      paddingLeft: insets.left + theme.spacing.m,
      paddingRight: insets.right + theme.spacing.m,
    }}>
      {/* Screen content */}
    </View>
  );
}

Customizing themes based on platform is crucial for meeting platform-specific design guidelines while maintaining a cohesive experience.

Performance Considerations

Performance Benchmarks

In our testing, theme switching should meet the following performance benchmarks:

OperationTarget DurationNotes
Initial theme load< 50msFrom cached source
Remote theme load< 250msFirst load with caching
Theme switch< 100msIncluding UI updates
Dynamic color access< 1msSingle theme property

These benchmarks provide targets to maintain a responsive user experience. If your implementation exceeds these times, consider the optimization strategies below.

Optimization Strategies

  1. Memoize theme-dependent values

    // Using useMemo to avoid recreating styles on every render
    const styles = React.useMemo(() => {
      return StyleSheet.create({
        container: {
          backgroundColor: theme.colors.background,
          padding: theme.spacing.m,
        },
        // Other styles...
      });
    }, [theme.colors.background, theme.spacing.m]);
  2. Pre-calculate derived values

    // In your theme definition, pre-calculate values that
    // would otherwise be calculated multiple times
    const theme = {
      colors: {
        primary: '#1976D2',
        // Derived colors pre-calculated
        primaryLight: lightenColor('#1976D2', 0.2),
        primaryDark: darkenColor('#1976D2', 0.2),
      },
      // Other theme properties...
    };

Theme management performance is critical for maintaining a responsive user experience, especially during theme switching.

Prerequisites

Before implementing themes, ensure you are familiar with:

  • Theme Management - Core concepts and architecture
  • React Context API for state management
  • React Native's StyleSheet API
  • TypeScript type definitions

Implementation Details

Theme Definition

Themes are defined in core/shared/styles/theme.ts with TypeScript typings:

// core/shared/styles/theme.ts
import { Platform } from 'react-native';
 
// Base color palette
export const baseColors = {
  // Primary palette
  primary: '#0066CC',
  primaryLight: '#4D94DB',
  primaryDark: '#004C99',
  
  // Secondary palette
  secondary: '#FF6B00',
  secondaryLight: '#FF9D4D',
  secondaryDark: '#CC5500',
  
  // Neutral palette
  background: '#FFFFFF',
  surface: '#F8F8F8',
  text: '#212121',
  textSecondary: '#757575',
  border: '#E0E0E0',
  
  // Feedback colors
  success: '#4CAF50',
  warning: '#FFC107',
  error: '#F44336',
  info: '#2196F3',
};
 
// Spacing scale
export const baseSpacing = {
  xs: 4,
  sm: 8,
  md: 16,
  lg: 24,
  xl: 32,
  xxl: 48,
};
 
// Border radius scale
export const baseRadius = {
  xs: 2,
  sm: 4,
  md: 8,
  lg: 16,
  round: 9999,
};
 
// Base theme used as the foundation
export const baseTheme = {
  colors: baseColors,
  spacing: baseSpacing,
  radius: baseRadius,
};
 
// Light theme extends base theme
export const lightTheme = {
  ...baseTheme,
  colors: {
    ...baseTheme.colors,
    // Light-mode specific overrides if needed
  },
};
 
// Dark theme extends base theme with dark mode overrides
export const darkTheme = {
  ...baseTheme,
  colors: {
    ...baseTheme.colors,
    background: '#121212',
    surface: '#1E1E1E',
    text: '#FFFFFF',
    textSecondary: '#BBBBBB',
    border: '#333333',
    // Other dark-mode specific overrides
  },
};

Typography Definition

Typography styles are defined in a separate file to maintain clear organization:

// core/shared/styles/typography.ts
import { Platform } from 'react-native';
 
// Font families
export const fontFamilies = {
  regular: Platform.select({
    ios: 'System',
    android: 'Roboto',
    default: 'System',
  }),
  medium: Platform.select({
    ios: 'System',
    android: 'Roboto-Medium',
    default: 'System',
  }),
  bold: Platform.select({
    ios: 'System',
    android: 'Roboto-Bold',
    default: 'System',
  }),
};
 
// Font sizes
export const fontSizes = {
  xs: 12,
  sm: 14,
  md: 16,
  lg: 18,
  xl: 20,
  xxl: 24,
  xxxl: 30,
};
 
// Line heights - usually 1.5x font size for good readability
export const lineHeights = {
  xs: 18,
  sm: 21,
  md: 24,
  lg: 27,
  xl: 30,
  xxl: 36,
  xxxl: 45,
};
 
// Text styles compositions
export const textStyles = {
  h1: {
    fontFamily: fontFamilies.bold,
    fontSize: fontSizes.xxxl,
    lineHeight: lineHeights.xxxl,
  },
  h2: {
    fontFamily: fontFamilies.bold,
    fontSize: fontSizes.xxl,
    lineHeight: lineHeights.xxl,
  },
  h3: {
    fontFamily: fontFamilies.bold,
    fontSize: fontSizes.xl,
    lineHeight: lineHeights.xl,
  },
  body: {
    fontFamily: fontFamilies.regular,
    fontSize: fontSizes.md,
    lineHeight: lineHeights.md,
  },
  bodySmall: {
    fontFamily: fontFamilies.regular,
    fontSize: fontSizes.sm,
    lineHeight: lineHeights.sm,
  },
  button: {
    fontFamily: fontFamilies.medium,
    fontSize: fontSizes.md,
    lineHeight: lineHeights.md,
  },
  caption: {
    fontFamily: fontFamilies.regular,
    fontSize: fontSizes.xs,
    lineHeight: lineHeights.xs,
  },
};

Theme Provider Implementation

The ThemeProvider component leverages React Context to provide theme values to the component tree:

// core/shared/styles/ThemeProvider.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { lightTheme, darkTheme } from './theme';
import { textStyles } from './typography';
 
// Theme context type definition
export interface ThemeContextType {
  colors: typeof lightTheme.colors;
  spacing: typeof lightTheme.spacing;
  radius: typeof lightTheme.radius;
  textStyles: typeof textStyles;
  isDark: boolean | null; // null means system default
  toggleThemeMode: () => void;
  setThemeMode: (mode: 'light' | 'dark' | 'system') => void;
}
 
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
 
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};
 
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  // Get device color scheme
  const deviceColorScheme = useColorScheme();
  
  // Theme mode preference (light, dark, or system)
  const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>('system');
  
  // Determine if dark mode is active
  const isDark = React.useMemo(() => {
    if (themeMode === 'system') {
      return deviceColorScheme === 'dark';
    }
    return themeMode === 'dark';
  }, [themeMode, deviceColorScheme]);
  
  // Get the base color theme based on dark/light
  const baseColorTheme = React.useMemo(() => {
    return isDark ? darkTheme : lightTheme;
  }, [isDark]);
  
  // Merge with brand theme if available
  // Note: In a full implementation, you would include brand theme merging here
  const theme = React.useMemo(() => {
    const mergedTheme = {
      ...baseColorTheme,
      textStyles,
    };
    
    return mergedTheme;
  }, [baseColorTheme]);
  
  // Toggle between light and dark mode
  const toggleThemeMode = () => {
    if (themeMode === 'light') {
      setThemeMode('dark');
    } else if (themeMode === 'dark') {
      setThemeMode('system');
    } else {
      setThemeMode('light');
    }
  };
  
  const contextValue: ThemeContextType = {
    ...theme,
    isDark,
    toggleThemeMode,
    setThemeMode,
  };
  
  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

Using Themes in Components

Components can consume the theme through the provided hooks:

// ui/components/Button.tsx
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import { useTheme } from '@/core/shared/styles/ThemeProvider';
 
interface ButtonProps {
  title: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary' | 'outline';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
}
 
export function Button({
  title,
  onPress,
  variant = 'primary',
  size = 'medium',
  disabled = false,
}: ButtonProps) {
  const theme = useTheme();
  
  // Determine button styling based on variant
  const getButtonStyle = () => {
    switch (variant) {
      case 'primary':
        return {
          backgroundColor: theme.colors.primary,
          borderColor: theme.colors.primary,
        };
      case 'secondary':
        return {
          backgroundColor: theme.colors.secondary,
          borderColor: theme.colors.secondary,
        };
      case 'outline':
        return {
          backgroundColor: 'transparent',
          borderColor: theme.colors.primary,
        };
      default:
        return {
          backgroundColor: theme.colors.primary,
          borderColor: theme.colors.primary,
        };
    }
  };
  
  // Determine text color based on variant
  const getTextColor = () => {
    if (variant === 'outline') {
      return theme.colors.primary;
    }
    return '#FFFFFF';
  };
  
  // Determine padding based on size
  const getPadding = () => {
    switch (size) {
      case 'small':
        return theme.spacing.sm;
      case 'large':
        return theme.spacing.lg;
      default:
        return theme.spacing.md;
    }
  };
  
  return (
    <TouchableOpacity
      style={[
        styles.button,
        {
          ...getButtonStyle(),
          padding: getPadding(),
          borderRadius: theme.radius.md,
          opacity: disabled ? 0.6 : 1,
        },
      ]}
      onPress={onPress}
      disabled={disabled}
    >
      <Text
        style={[
          theme.textStyles.button,
          {
            color: getTextColor(),
          },
        ]}
      >
        {title}
      </Text>
    </TouchableOpacity>
  );
}
 
const styles = StyleSheet.create({
  button: {
    alignItems: 'center',
    justifyContent: 'center',
    borderWidth: 1,
  },
});

Usage Patterns

Responsive Theme Utilities

Create responsive styling utilities to adapt to different screen sizes:

// core/shared/hooks/useResponsiveStyles.ts
import { useWindowDimensions } from 'react-native';
import { useTheme } from '@/core/shared/styles/ThemeProvider';
 
export function useResponsiveStyles() {
  const { width, height } = useWindowDimensions();
  const theme = useTheme();
  
  // Breakpoints
  const isSmallDevice = width < 375;
  const isMediumDevice = width >= 375 && width < 768;
  const isLargeDevice = width >= 768;
  
  // Responsive spacing
  const getResponsiveSpacing = (size: keyof typeof theme.spacing) => {
    const baseSpacing = theme.spacing[size];
    
    if (isSmallDevice) {
      return Math.max(baseSpacing * 0.8, 4); // Min 4px
    }
    if (isLargeDevice) {
      return baseSpacing * 1.2;
    }
    return baseSpacing;
  };
  
  // Responsive font sizing
  const getResponsiveFontSize = (size: number) => {
    if (isSmallDevice) {
      return size * 0.9;
    }
    if (isLargeDevice) {
      return size * 1.1;
    }
    return size;
  };
  
  return {
    isSmallDevice,
    isMediumDevice,
    isLargeDevice,
    getResponsiveSpacing,
    getResponsiveFontSize,
    screenWidth: width,
    screenHeight: height,
  };
}

Theme Switcher Component

Create a component to allow users to switch between themes:

// ui/components/ThemeSwitcher.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useTheme } from '@/core/shared/styles/ThemeProvider';
import { Icon } from '@/ui/components/Icon';
 
export function ThemeSwitcher() {
  const { isDark, toggleThemeMode, setThemeMode } = useTheme();
  
  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={[styles.option, isDark === false && styles.selected]}
        onPress={() => setThemeMode('light')}
      >
        <Icon name="sun" />
        <Text>Light</Text>
      </TouchableOpacity>
      
      <TouchableOpacity
        style={[styles.option, isDark === true && styles.selected]}
        onPress={() => setThemeMode('dark')}
      >
        <Icon name="moon" />
        <Text>Dark</Text>
      </TouchableOpacity>
      
      <TouchableOpacity
        style={[styles.option, isDark === null && styles.selected]}
        onPress={() => setThemeMode('system')}
      >
        <Icon name="smartphone" />
        <Text>System</Text>
      </TouchableOpacity>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    padding: 16,
  },
  option: {
    alignItems: 'center',
    padding: 8,
  },
  selected: {
    borderBottomWidth: 2,
    borderBottomColor: '#0066CC',
  },
});

Best Practices & Security

Performance Optimization

  • (Do ✅) Memoize theme values

    // Good practice
    const buttonStyles = React.useMemo(() => ({
      container: {
        backgroundColor: theme.colors.primary,
        padding: theme.spacing.md,
      },
      text: {
        color: '#fff',
        ...theme.textStyles.button,
      },
    }), [theme.colors.primary, theme.spacing.md, theme.textStyles.button]);
  • (Do ✅) Use StyleSheet.create for static styles

    // Static styles that don't depend on theme
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        alignItems: 'center',
      },
    });
     
    // Then combine with dynamic styles
    <View style={[styles.container, { backgroundColor: theme.colors.background }]} />
  • (Don't ❌) Create style objects in render

    // Avoid this pattern
    return (
      <View style={{ 
        margin: theme.spacing.md, // Creates new object on every render
        padding: theme.spacing.sm,
        backgroundColor: theme.colors.surface,
      }} />
    );

Troubleshooting

Common Issues and Solutions

IssuePossible CauseSolution
Theme not applying to componentsComponent not wrapped in ThemeProviderEnsure the component is a descendant of ThemeProvider
Error: "useTheme must be used within a ThemeProvider"Using useTheme outside provider scopeCheck component hierarchy or add ThemeProvider higher in the tree
Inconsistent colors across appMultiple theme definitionsEnsure all theme values come from the central theme system
Theme not updating on mode changeMissing dependencies in useMemoAdd all theme dependencies to dependency arrays
Slow performance with themesCreating style objects in renderMove style creation outside render or use StyleSheet.create

Migration Considerations

When migrating from hardcoded styles to the theme system:

  1. Audit existing styles

    • Identify all hardcoded colors, spacing values, and text styles
    • Map them to corresponding theme values
  2. Incremental migration approach

    • Start with core components used throughout the app
    • Work outward to more specialized components
    • Update one component family at a time
  3. Unit test coverage

    • Add tests for theme-dependent rendering
    • Verify components render correctly in both light and dark modes
  4. Design system alignment

    • Ensure the theme values match the design system specifications
    • Create a theme validator utility if needed

Core References

Architecture Documents

UI Components

Summary

Implementing the theme system in your components involves:

  1. Setting up the ThemeProvider at the application root
  2. Using the useTheme hook to access theme values in components
  3. Creating well-structured, maintainable styles that derive from theme values
  4. Optimizing performance by avoiding inline style creation
  5. Supporting accessibility with proper contrast and text scaling

By following these patterns consistently, you ensure a cohesive visual experience across the application and make it easier to implement brand-specific theming.