Dark Mode & Theming in Rork Max
Dark mode has evolved from a nice-to-have feature to an essential requirement. Users expect apps to respect their system preferences, reduce eye strain during evening use, and conserve battery on OLED displays. Creating a themeable app demonstrates attention to user experience and accessibility.
Organizing Design Tokens
Color Palette and Design System
The foundation of effective theming is centralizing design tokens—colors, spacing, typography, and other design values.
// theme/tokens.js
const lightTheme = {
colors: {
primary: '#0066CC',
secondary: '#FF6B6B',
background: '#FFFFFF',
surface: '#F5F5F5',
text: '#222222',
textSecondary: '#666666',
border: '#DDDDDD',
success: '#22C55E',
warning: '#FBBF24',
error: '#EF4444',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
fontSize: {
sm: 12,
base: 14,
lg: 16,
xl: 20,
'2xl': 24,
},
};
const darkTheme = {
colors: {
primary: '#4A9EFF',
secondary: '#FF8A9B',
background: '#1A1A1A',
surface: '#2D2D2D',
text: '#FFFFFF',
textSecondary: '#CCCCCC',
border: '#404040',
success: '#4ADE80',
warning: '#FACC15',
error: '#F87171',
},
spacing: lightTheme.spacing,
fontSize: lightTheme.fontSize,
};
export { lightTheme, darkTheme };Centralizing tokens eliminates duplication and makes theme switching effortless.
Managing Global Theme State
Context API for Theme Distribution
React Context allows you to make the current theme accessible throughout your app without prop drilling.
import { createContext, useState, useContext, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { lightTheme, darkTheme } from './tokens';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const systemColorScheme = useColorScheme();
const [isDarkMode, setIsDarkMode] = useState(
systemColorScheme === 'dark'
);
// Sync with system preferences
useEffect(() => {
if (systemColorScheme) {
setIsDarkMode(systemColorScheme === 'dark');
console.log(`System theme detected: ${systemColorScheme}`);
// Expected output: System theme detected: dark
}
}, [systemColorScheme]);
const theme = isDarkMode ? darkTheme : lightTheme;
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
console.log(`Theme toggled to: ${!isDarkMode ? 'dark' : 'light'}`);
// Expected output: Theme toggled to: dark
};
const value = {
isDarkMode,
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}Applying Themes to Components
Using Theme Values in Components
Components consume theme values from the context hook, automatically updating when the theme changes.
import { useTheme } from './ThemeProvider';
import { View, Text, StyleSheet } from 'react-native';
export function ThemedCard({ title, description }) {
const { theme } = useTheme();
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border,
borderWidth: 1,
borderRadius: 8,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
},
title: {
color: theme.colors.text,
fontSize: theme.fontSize.lg,
fontWeight: 'bold',
marginBottom: theme.spacing.sm,
},
description: {
color: theme.colors.textSecondary,
fontSize: theme.fontSize.base,
lineHeight: 20,
},
});
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
);
}This pattern ensures components always use the correct colors for the current theme.
Persisting User Preferences
Saving Theme Selection
Users expect their theme choice to persist across app sessions. Store preferences locally.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect } from 'react';
const THEME_PREFERENCE_KEY = '@rork_theme_preference';
export function usePersistentTheme() {
const { isDarkMode, toggleTheme } = useTheme();
// Load saved preference on startup
useEffect(() => {
loadThemePreference();
}, []);
// Save whenever theme changes
useEffect(() => {
saveThemePreference(isDarkMode);
}, [isDarkMode]);
const loadThemePreference = async () => {
try {
const savedTheme = await AsyncStorage.getItem(THEME_PREFERENCE_KEY);
if (savedTheme !== null) {
const isDark = savedTheme === 'dark';
if (isDark !== isDarkMode) {
toggleTheme();
}
console.log(`Loaded saved theme: ${savedTheme}`);
// Expected output: Loaded saved theme: dark
}
} catch (error) {
console.error('Theme load error:', error);
}
};
const saveThemePreference = async (isDark) => {
try {
const themeValue = isDark ? 'dark' : 'light';
await AsyncStorage.setItem(THEME_PREFERENCE_KEY, themeValue);
} catch (error) {
console.error('Theme save error:', error);
}
};
}Dark Mode Best Practices
Accessibility and Contrast
Sufficient color contrast is critical for readability and accessibility compliance.
// Ensure adequate contrast ratios
const theme = {
colors: {
// Light mode
lightText: '#222222', // Against #FFFFFF: 17:1 ratio (WCAG AAA)
lightBg: '#FFFFFF',
// Dark mode
darkText: '#FFFFFF', // Against #1A1A1A: 16:1 ratio (WCAG AAA)
darkBg: '#1A1A1A',
},
};
export function AccessibleText({ children, isDark }) {
const color = isDark ? theme.colors.darkText : theme.colors.lightText;
return (
<Text style={{ color }}>
{children}
</Text>
);
}WCAG 2.1 guidelines recommend a minimum 4.5:1 contrast ratio for normal text. Aim for 7:1 when possible.
Building Custom Themes
User-Defined Theme Creation
Enable advanced users to create personalized themes.
import { useState } from 'react';
export function useCustomTheme() {
const [customThemes, setCustomThemes] = useState([]);
const createCustomTheme = (name, colors) => {
const newTheme = {
id: Date.now(),
name,
colors: {
primary: colors.primary,
secondary: colors.secondary,
background: colors.background,
surface: colors.surface,
text: colors.text,
textSecondary: colors.textSecondary,
},
};
setCustomThemes([...customThemes, newTheme]);
console.log(`Custom theme created: ${name}`);
// Expected output: Custom theme created: MyCyan
return newTheme;
};
const deleteCustomTheme = (themeId) => {
setCustomThemes(customThemes.filter(t => t.id !== themeId));
};
return {
customThemes,
createCustomTheme,
deleteCustomTheme,
};
}
export function CustomThemeEditor() {
const [themeName, setThemeName] = useState('');
const [primaryColor, setPrimaryColor] = useState('#0066CC');
const { createCustomTheme } = useCustomTheme();
const handleCreate = () => {
if (themeName) {
createCustomTheme(themeName, {
primary: primaryColor,
secondary: '#FF6B6B',
background: '#FFFFFF',
surface: '#F5F5F5',
text: '#222222',
textSecondary: '#666666',
});
setThemeName('');
}
};
return (
<div style={{ padding: '20px' }}>
<input
type="text"
value={themeName}
onChange={(e) => setThemeName(e.target.value)}
placeholder="Theme name"
/>
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
/>
<button onClick={handleCreate}>Create Theme</button>
</div>
);
}Integration with Other Features
Applying consistent theming across camera and gallery screens is covered in Rork Max Camera & Gallery Guide.
For comprehensive design system alignment with design tools, see Rork Max Figma Integration Guide. Learn how to animate theme transitions smoothly in Rork Max Animations Guide.