Theme System

The mobile app supports dark and light themes using React Context, providing a seamless visual experience across all components with preference persistence.


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    THEME ARCHITECTURE                        β”‚
β”‚                                                              β”‚
β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”‚
β”‚           β”‚  ThemeProvider  β”‚ ← User Toggle                  β”‚
β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚
β”‚                    β”‚                                         β”‚
β”‚                    β–Ό                                         β”‚
β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”‚
β”‚           β”‚  ThemeContext   β”‚                                β”‚
β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚
β”‚                    β”‚                                         β”‚
β”‚                    β–Ό                                         β”‚
β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”‚
β”‚           β”‚  useTheme Hook  β”‚                                β”‚
β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚
β”‚                    β”‚                                         β”‚
β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚     β–Ό              β–Ό              β–Ό              β–Ό          β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚HomeScreenβ”‚ β”‚IngredientCardβ”‚ β”‚ProfileSelectorβ”‚ β”‚ Results β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Usage

Access Theme in Components

import { useTheme } from '../context/ThemeContext';

function MyComponent() {
  const { theme, themeMode, toggleTheme } = useTheme();

  return (
    <View style={{ backgroundColor: theme.colors.background }}>
      <Text style={{ color: theme.colors.textPrimary }}>
        Hello World
      </Text>
      <Button onPress={toggleTheme}>
        Toggle Theme
      </Button>
    </View>
  );
}

ThemeContext API

interface ThemeContextType {
  theme: Theme;              // Current color scheme object
  themeMode: ThemeMode;      // 'light' | 'dark'
  toggleTheme: () => void;   // Switch between modes
  setThemeMode: (mode: ThemeMode) => void;  // Set specific mode
}

Color Schemes

Light Theme

export const lightTheme = {
  mode: 'light',
  colors: {
    // Backgrounds
    background: '#f8fafc',
    card: '#ffffff',
    cardBorder: '#e5e7eb',
    inputBackground: '#f9fafb',

    // Text
    textPrimary: '#1f2937',
    textSecondary: '#6b7280',
    textMuted: '#9ca3af',

    // Accent colors
    primary: '#6366f1',
    success: '#22c55e',
    warning: '#f59e0b',
    danger: '#ef4444',
    info: '#3b82f6',

    // UI elements
    divider: '#e5e7eb',
    shadow: '#000000',
    overlay: 'rgba(0,0,0,0.5)',
  },
};

Dark Theme

export const darkTheme = {
  mode: 'dark',
  colors: {
    // Backgrounds
    background: '#0f172a',
    card: '#1e293b',
    cardBorder: '#334155',
    inputBackground: '#1e293b',

    // Text
    textPrimary: '#f1f5f9',
    textSecondary: '#94a3b8',
    textMuted: '#64748b',

    // Accent colors
    primary: '#818cf8',
    success: '#4ade80',
    warning: '#fbbf24',
    danger: '#f87171',
    info: '#60a5fa',

    // UI elements
    divider: '#334155',
    shadow: '#000000',
    overlay: 'rgba(0,0,0,0.7)',
  },
};

Color Comparison

TokenLightDark
background #f8fafc #0f172a
card #ffffff #1e293b
textPrimary #1f2937 #f1f5f9
primary #6366f1 #818cf8
success #22c55e #4ade80
danger #ef4444 #f87171

Implementation

ThemeProvider

Wrap your app with ThemeProvider:

// App.tsx
import { ThemeProvider } from './src/context/ThemeContext';

export default function App() {
  return (
    <ThemeProvider>
      <SafeAreaProvider>
        <HomeScreen />
      </SafeAreaProvider>
    </ThemeProvider>
  );
}

ThemeContext Source

// context/ThemeContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { ThemeMode } from '../types';

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [themeMode, setThemeMode] = useState<ThemeMode>('light');

  const theme = themeMode === 'light' ? lightTheme : darkTheme;

  const toggleTheme = () => {
    setThemeMode(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, themeMode, toggleTheme, setThemeMode }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

Styling Patterns

Dynamic Styles

function Card() {
  const { theme } = useTheme();

  return (
    <View style={[
      styles.card,
      {
        backgroundColor: theme.colors.card,
        borderColor: theme.colors.cardBorder,
      }
    ]}>
      <Text style={{ color: theme.colors.textPrimary }}>
        Content
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    borderRadius: 12,
    padding: 16,
    borderWidth: 1,
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 3,
  },
});

StatusBar Integration

import { StatusBar } from 'expo-status-bar';

function App() {
  const { themeMode } = useTheme();

  return (
    <>
      <StatusBar style={themeMode === 'dark' ? 'light' : 'dark'} />
      <HomeScreen />
    </>
  );
}

Best Practices

1. Use Semantic Tokens
// βœ… Good - semantic meaning
<Text style={{ color: theme.colors.textPrimary }}>Title</Text>
<Text style={{ color: theme.colors.textSecondary }}>Subtitle</Text>

// ❌ Bad - hardcoded colors
<Text style={{ color: '#1f2937' }}>Title</Text>
2. Extract Theme in Component Root
// βœ… Good - single hook call
function MyComponent() {
  const { theme } = useTheme();
  return (
    <View style={{ backgroundColor: theme.colors.background }}>
      <Text style={{ color: theme.colors.textPrimary }}>...</Text>
    </View>
  );
}

// ❌ Bad - multiple hook calls in render
function MyComponent() {
  return (
    <View style={{ backgroundColor: useTheme().theme.colors.background }}>
      <Text style={{ color: useTheme().theme.colors.textPrimary }}>...</Text>
    </View>
  );
}
3. Consistent Shadows
const getShadow = (theme: Theme) => ({
  shadowColor: theme.colors.shadow,
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: theme.mode === 'dark' ? 0.3 : 0.1,
  shadowRadius: 8,
  elevation: 3,
});

Related Documentation