Customization & Theming
Complete guide to customizing themes, colors, and design tokens in the Expo boilerplate.
This guide covers how to customize the visual appearance of your app, including creating new themes, modifying colors, and understanding the design token system.
Architecture Overview
The theming system is built on design tokens - a set of semantic variables that define colors, typography, spacing, and other visual properties. Each theme is completely self-contained with its own color palette, making customization straightforward.
File Responsibilities
| File | Purpose |
|---|---|
design-tokens.ts | Backward-compatible re-export from themes |
themes/index.ts | Barrel file that exports all theming components |
app-theme.ts | AppThemeRegistry for accessing tokens |
design-tokens.model.ts | DesignTokens interface and AppTheme enum |
text-styles.ts | Typography scale and text style definitions |
spacing.ts | Spacing, padding, layout, and size tokens |
colors.ts | AppColors palette and ThemeColorVariables |
*theme.ts | Self-contained theme with *Colors object + tokens |
Built-in Themes
The boilerplate includes four pre-built themes, each with its own color palette:
| Theme | Description | Border Radius | Color Object |
|---|---|---|---|
| Modern | Clean, professional SaaS aesthetic | Medium (8px) | ModernColors |
| Retro | Bold, nostalgic vintage feel | Sharp (0px) | RetroColors |
| Cozy | Warm, inviting with soft corners | Large (18px) | CozyColors |
| Paper | Minimalist with pure scaffolds | Large (18px) | PaperColors |
Each theme supports both light and dark modes automatically.
Using Design Tokens
Access design tokens anywhere in your app using the useTheme hook:
import { useTheme } from '@/constants/theme-context';
export function MyComponent() {
const { tokens } = useTheme();
return (
<View
style={{
padding: tokens.layout.contentPadding,
backgroundColor: tokens.cardBackground,
borderRadius: tokens.cardBorderRadius,
}}
>
<Text style={tokens.bodyPrimary}>Hello World</Text>
</View>
);
}Available Token Categories
// Primary & Secondary
tokens.primary // Brand primary color
tokens.onPrimary // Text/icons on primary
tokens.secondary // Brand secondary color
// Semantic Colors
tokens.success // Success/positive
tokens.error // Error/negative
tokens.warning // Warning/caution
tokens.info // Informational
// Surface Colors
tokens.background // Main background
tokens.surface // Cards/elevated surfaces
tokens.onSurface // Text/icons on surface
// Variants & Utility
tokens.surfaceVariant
tokens.muted // Disabled/placeholder
tokens.outline // Borders/dividersCreating a New Theme
Each theme is self-contained in a single file with its own color palette. Follow these steps to add a custom theme.
Create Theme File
Create a new file in constants/themes/definitions/:
/**
* Ocean Theme
*
* A calming, aquatic theme inspired by the ocean.
*/
import { AppTheme, DesignTokens } from '../base/design-tokens.model';
import { AppTextStyles, ModernTypography } from '../base/text-styles';
import { BorderWidth, ComponentSizes, Layout, Padding, Spacing } from '../base/spacing';
// =============================================================================
// OCEAN THEME COLORS
// =============================================================================
/** Ocean theme color palette */
export const OceanColors = {
// Primary & Secondary
primary: '#0077B6',
primaryDark: '#00B4D8',
secondary: '#90E0EF',
secondaryDark: '#CAF0F8',
// Surface
surfaceLight: '#F0F9FF',
borderLight: '#ADE8F4',
borderAccentLight: '#E0F7FF',
borderAccentDark: '#023E8A',
} as const;
// Shared colors (copy from existing theme)
const SharedColors = {
success: '#10B981',
successDark: '#6EE7B7',
error: '#EF4444',
errorDark: '#FCA5A5',
warning: '#F59E0B',
warningDark: '#FCD34D',
info: '#3B82F6',
infoDark: '#60A5FA',
background: '#FFFFFF',
backgroundDark: '#03045E',
surface: '#FFFFFF',
surfaceDark: '#023E8A',
borderDark: '#0077B6',
borderSoft: 'rgba(0, 0, 0, 0.05)',
borderSoftDark: 'rgba(255, 255, 255, 0.05)',
onPrimary: '#FFFFFF',
onPrimaryDark: '#000000',
onSurface: '#000000',
onSurfaceDark: '#FFFFFF',
muted: '#737373',
mutedDark: '#a1a1a1',
surfaceVariant: '#F5F5F5',
surfaceVariantDark: '#2A2A2A',
outline: '#9CA3AF',
outlineDark: '#6B7280',
} as const;
// =============================================================================
// OCEAN THEME TOKENS
// =============================================================================
/** Ocean theme (light mode) */
export const oceanLightTokens: DesignTokens = {
theme: AppTheme.Ocean, // Will be added in next step
brightness: 'light',
primary: OceanColors.primary,
onPrimary: SharedColors.onPrimary,
secondary: OceanColors.secondary,
success: SharedColors.success,
error: SharedColors.error,
warning: SharedColors.warning,
info: SharedColors.info,
background: SharedColors.background,
surface: OceanColors.surfaceLight,
surfaceVariant: SharedColors.surfaceVariant,
outline: SharedColors.outline,
muted: SharedColors.muted,
onSurface: SharedColors.onSurface,
typography: ModernTypography,
bodyPrimary: AppTextStyles.bodyPrimary,
bodySecondary: AppTextStyles.bodySecondary,
mutedText: AppTextStyles.mutedText,
buttonBorderRadius: 12,
cardBorderRadius: 16,
navbarBorderRadius: 24,
spacing: Spacing,
padding: Padding,
layout: Layout,
componentSizes: ComponentSizes,
borderWidths: BorderWidth,
borderColors: {
default: OceanColors.borderLight,
accent: OceanColors.borderAccentLight,
muted: SharedColors.muted,
soft: SharedColors.borderSoft,
error: SharedColors.error,
},
};
/** Ocean theme (dark mode) */
export const oceanDarkTokens: DesignTokens = {
theme: AppTheme.Ocean,
brightness: 'dark',
primary: OceanColors.primaryDark,
onPrimary: SharedColors.onPrimaryDark,
secondary: OceanColors.secondaryDark,
success: SharedColors.successDark,
error: SharedColors.errorDark,
warning: SharedColors.warningDark,
info: SharedColors.infoDark,
background: SharedColors.backgroundDark,
surface: SharedColors.surfaceDark,
surfaceVariant: SharedColors.surfaceVariantDark,
outline: SharedColors.outlineDark,
muted: SharedColors.mutedDark,
onSurface: SharedColors.onSurfaceDark,
typography: ModernTypography,
bodyPrimary: AppTextStyles.bodyPrimaryDark,
bodySecondary: AppTextStyles.bodySecondaryDark,
mutedText: AppTextStyles.mutedTextDark,
buttonBorderRadius: 12,
cardBorderRadius: 16,
navbarBorderRadius: 24,
spacing: Spacing,
padding: Padding,
layout: Layout,
componentSizes: ComponentSizes,
borderWidths: BorderWidth,
borderColors: {
default: SharedColors.borderDark,
accent: OceanColors.borderAccentDark,
muted: SharedColors.mutedDark,
soft: SharedColors.borderSoftDark,
error: SharedColors.errorDark,
},
};Register the Theme
Update design-tokens.model.ts to add the theme enum:
/** Supported application themes */
export enum AppTheme {
Modern = 'modern',
Retro = 'retro',
Cozy = 'cozy',
Paper = 'paper',
Ocean = 'ocean', // Add your theme here
}Update app-theme.ts to include your theme in the maps:
import { oceanLightTokens, oceanDarkTokens } from './definitions/ocean.theme';
const lightThemeMap: Record<AppTheme, DesignTokens> = {
[AppTheme.Modern]: modernLightTokens,
[AppTheme.Retro]: retroLightTokens,
[AppTheme.Cozy]: cozyLightTokens,
[AppTheme.Paper]: paperLightTokens,
[AppTheme.Ocean]: oceanLightTokens, // Add here
};
const darkThemeMap: Record<AppTheme, DesignTokens> = {
[AppTheme.Modern]: modernDarkTokens,
[AppTheme.Retro]: retroDarkTokens,
[AppTheme.Cozy]: cozyDarkTokens,
[AppTheme.Paper]: paperDarkTokens,
[AppTheme.Ocean]: oceanDarkTokens, // Add here
};Export the Theme
Add export to the barrel file:
// Theme definitions
export * from './definitions/modern.theme';
export * from './definitions/retro.theme';
export * from './definitions/cozy.theme';
export * from './definitions/paper.theme';
export * from './definitions/ocean.theme'; // Add thisAdd to Theme Selector
Update the appearance settings to include your theme:
const THEME_STYLE_OPTIONS: { style: AppTheme; label: string }[] = [
{ style: AppTheme.Modern, label: t('appearance.styles.modern') },
{ style: AppTheme.Retro, label: t('appearance.styles.retro') },
{ style: AppTheme.Cozy, label: t('appearance.styles.cozy') },
{ style: AppTheme.Paper, label: t('appearance.styles.paper') },
{ style: AppTheme.Ocean, label: 'Ocean' }, // Add here
];Customizing Existing Themes
Each theme is self-contained in its own file, making customization easy.
Changing Primary Colors
Modify the color constants in the theme's *Colors object:
export const ModernColors = {
// Before: Blue primary
primary: '#7aaae6',
// After: Green primary
primary: '#22C55E',
};Changing Border Radii
Modify the layout tokens in the theme definition:
export const modernLightTokens: DesignTokens = {
// ...other tokens...
// Change from 8 to 16 for more rounded corners
buttonBorderRadius: 16,
cardBorderRadius: 16,
navbarBorderRadius: 20,
};Changing Semantic Colors Per Theme
Since each theme defines its own colors, you can have different semantic colors:
const SharedColors = {
// Retro theme uses different success color for vintage feel
success: '#059669', // Slightly different green
error: '#DC2626', // Different red shade
};Accent Color System
The app supports dynamic accent colors that users can choose at runtime. These override certain colors based on the selected accent:
import { ACCENT_COLORS, useTheme } from '@/constants/theme-context';
export function MyComponent() {
const { accentColor, setAccentColor, tokens } = useTheme();
return (
<View style={{ backgroundColor: tokens.accentSurface }}>
{ACCENT_COLORS.map(color => (
<Pressable
key={color.id}
onPress={() => setAccentColor(color)}
style={{ backgroundColor: color.color }}
/>
))}
</View>
);
}Available Accent Colors
| ID | Name | Color |
|---|---|---|
green | Green | #10B981 |
blue | Blue | #3B82F6 |
indigo | Indigo | #6366F1 |
purple | Purple | #A855F7 |
pink | Pink | #EC4899 |
amber | Amber | #F59E0B |
Adding More Accent Colors
Extend the ACCENT_COLORS array in theme-context.tsx:
export const ACCENT_COLORS: AccentColor[] = [
// Existing colors...
{ id: 'green', name: 'Green', color: '#10B981', ... },
// Add your colors
{
id: 'teal',
name: 'Teal',
color: '#14B8A6',
warmLight: '#F0FDFA',
warmDark: '#042F2E',
borderLight: '#CCFBF1',
borderDark: '#0D3D3D',
},
];Theme Persistence
Theme settings are automatically persisted using AsyncStorage:
// Settings are stored with these keys
STORAGE_KEYS.THEME_MODE // 'light' | 'dark' | 'system'
STORAGE_KEYS.THEME_STYLE // 'modern' | 'retro' | 'cozy' | 'paper'
STORAGE_KEYS.ACCENT_COLOR // Color IDSettings are loaded on app startup and applied automatically.
Best Practices
Do's
- ✅ Always use design tokens instead of hardcoded values
- ✅ Keep all theme colors in the theme's
*Colorsobject - ✅ Test themes in both light and dark modes
- ✅ Ensure sufficient contrast for accessibility
- ✅ Use the
useThemehook for reactive updates
Don'ts
- ❌ Don't bypass the token system with inline colors
- ❌ Don't forget to add dark mode variants
- ❌ Don't use colors that fail accessibility contrast checks
- ❌ Don't reference colors from other theme files (keep self-contained)
Color Tools & Resources
- Coolors - Color palette generator
- Material Design Color Tool - M3 theme builder
- Contrast Checker - WCAG contrast validation
- Color Hunt - Curated color palettes
- Realtime Colors - Preview colors in context
