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
| Token | Light | Dark |
|---|---|---|
| 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,
});