Skip to main content

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โ€‹

TokenLightDark
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,
});