Theme System
The mobile app supports dark and light themes using React Context, providing a seamless visual experience across all components.
Architectureโ
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)',
// Profile tags
profileTagBg: '#f0f9ff',
profileTagBorder: '#bae6fd',
profileTagText: '#0c4a6e',
profileTagLabel: '#0369a1',
// Allergy tags
allergyBg: '#fef2f2',
allergyBorder: '#fecaca',
allergyText: '#dc2626',
// Gallery button
galleryBg: '#ecfdf5',
galleryBorder: '#a7f3d0',
galleryText: '#059669',
},
};
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)',
// Profile tags
profileTagBg: '#1e3a5f',
profileTagBorder: '#3b82f6',
profileTagText: '#93c5fd',
profileTagLabel: '#60a5fa',
// Allergy tags
allergyBg: '#450a0a',
allergyBorder: '#b91c1c',
allergyText: '#fca5a5',
// Gallery button
galleryBg: '#064e3b',
galleryBorder: '#10b981',
galleryText: '#6ee7b7',
},
};
Color Comparison Tableโ
| Token | Light | Dark |
|---|---|---|
background | #f8fafc | #0f172a |
card | #ffffff | #1e293b |
textPrimary | #1f2937 | #f1f5f9 |
textSecondary | #6b7280 | #94a3b8 |
primary | #6366f1 | #818cf8 |
success | #22c55e | #4ade80 |
warning | #f59e0b | #fbbf24 |
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 />
</>
);
}
Conditional Colorsโ
function RecommendationBadge({ recommendation }: { recommendation: string }) {
const backgroundColor = {
'SAFE': '#dcfce7',
'CAUTION': '#fef9c3',
'AVOID': '#fee2e2',
}[recommendation] || '#f3f4f6';
const textColor = {
'SAFE': '#166534',
'CAUTION': '#854d0e',
'AVOID': '#991b1b',
}[recommendation] || '#374151';
return (
<View style={{ backgroundColor, padding: 8, borderRadius: 16 }}>
<Text style={{ color: textColor, fontWeight: '600' }}>
{recommendation}
</Text>
</View>
);
}
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,
});