GymApp includes a comprehensive theming system that automatically adapts to the user’s device color scheme preference, supporting both light and dark modes.
Theme Architecture
The theming system consists of three main parts:
Color and font constants (constants/theme.ts)
Theme detection hook (hooks/use-color-scheme.ts)
Theme-aware components (components/themed-*.tsx)
The theme automatically switches based on system preferences, with no additional configuration required.
Color Definitions
Colors are defined in constants/theme.ts with separate palettes for light and dark modes:
// constants/theme.ts
const tintColorLight = '#0a7ea4' ;
const tintColorDark = '#fff' ;
export const Colors = {
light: {
text: '#11181C' ,
background: '#fff' ,
tint: tintColorLight ,
icon: '#687076' ,
tabIconDefault: '#687076' ,
tabIconSelected: tintColorLight ,
},
dark: {
text: '#ECEDEE' ,
background: '#151718' ,
tint: tintColorDark ,
icon: '#9BA1A6' ,
tabIconDefault: '#9BA1A6' ,
tabIconSelected: tintColorDark ,
},
};
text : Primary text color for body content
background : Main background color for views
tint : Accent color for interactive elements
icon : Default icon color
tabIconDefault : Inactive tab icon color
tabIconSelected : Active tab icon color
All colors use hex values for consistency. Add new color keys as needed for your features.
Font System
Fonts are defined with platform-specific fallbacks:
// constants/theme.ts
import { Platform } from 'react-native' ;
export const Fonts = Platform . select ({
ios: {
/** iOS UIFontDescriptorSystemDesignDefault */
sans: 'system-ui' ,
/** iOS UIFontDescriptorSystemDesignSerif */
serif: 'ui-serif' ,
/** iOS UIFontDescriptorSystemDesignRounded */
rounded: 'ui-rounded' ,
/** iOS UIFontDescriptorSystemDesignMonospaced */
mono: 'ui-monospace' ,
},
default: {
sans: 'normal' ,
serif: 'serif' ,
rounded: 'normal' ,
mono: 'monospace' ,
},
web: {
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" ,
serif: "Georgia, 'Times New Roman', serif" ,
rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif" ,
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace" ,
},
});
Uses iOS system fonts with San Francisco variants:
sans: Standard SF Pro
rounded: SF Pro Rounded
mono: SF Mono
serif: New York
Uses Android system defaults:
sans: Roboto
mono: Roboto Mono
serif: Noto Serif
Uses web-safe font stacks with system font fallbacks.
Theme Hooks
useColorScheme
Detects the current color scheme:
// hooks/use-color-scheme.ts
export { useColorScheme } from 'react-native' ;
Returns 'light', 'dark', or null:
import { useColorScheme } from '@/hooks/use-color-scheme' ;
function MyComponent () {
const colorScheme = useColorScheme ();
// colorScheme = 'light' | 'dark' | null
return < Text > Current theme: { colorScheme } </ Text > ;
}
useThemeColor
Returns colors that adapt to the current theme:
// hooks/use-theme-color.ts
import { Colors } from '@/constants/theme' ;
import { useColorScheme } from '@/hooks/use-color-scheme' ;
export function useThemeColor (
props : { light ?: string ; dark ?: string },
colorName : keyof typeof Colors . light & keyof typeof Colors . dark
) {
const theme = useColorScheme () ?? 'light' ;
const colorFromProps = props [ theme ];
if ( colorFromProps ) {
return colorFromProps ;
} else {
return Colors [ theme ][ colorName ];
}
}
Usage:
import { useThemeColor } from '@/hooks/use-theme-color' ;
function MyComponent () {
// Use theme color with optional overrides
const backgroundColor = useThemeColor (
{ light: '#f0f0f0' , dark: '#2a2a2a' },
'background'
);
return < View style = { { backgroundColor } } /> ;
}
Themed Components
GymApp provides pre-built themed components that automatically adapt to the color scheme.
ThemedView
A View that uses the theme background color:
// components/themed-view.tsx
import { View , type ViewProps } from 'react-native' ;
import { useThemeColor } from '@/hooks/use-theme-color' ;
export type ThemedViewProps = ViewProps & {
lightColor ?: string ;
darkColor ?: string ;
};
export function ThemedView ({ style , lightColor , darkColor , ... otherProps } : ThemedViewProps ) {
const backgroundColor = useThemeColor ({ light: lightColor , dark: darkColor }, 'background' );
return < View style = { [{ backgroundColor }, style ] } { ... otherProps } /> ;
}
Usage:
import { ThemedView } from '@/components/themed-view' ;
// Uses theme background color
< ThemedView style = { { flex: 1 } } >
< Text > Content here </ Text >
</ ThemedView >
// Override with custom colors
< ThemedView
lightColor = "#f9f9f9"
darkColor = "#1a1a1a"
style = { { padding: 20 } }
>
< Text > Custom background </ Text >
</ ThemedView >
ThemedText
A Text component with theme-aware colors and typography:
// components/themed-text.tsx
import { StyleSheet , Text , type TextProps } from 'react-native' ;
import { useThemeColor } from '@/hooks/use-theme-color' ;
export type ThemedTextProps = TextProps & {
lightColor ?: string ;
darkColor ?: string ;
type ?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' ;
};
export function ThemedText ({
style ,
lightColor ,
darkColor ,
type = 'default' ,
... rest
} : ThemedTextProps ) {
const color = useThemeColor ({ light: lightColor , dark: darkColor }, 'text' );
return (
< Text
style = { [
{ color },
type === 'default' ? styles . default : undefined ,
type === 'title' ? styles . title : undefined ,
type === 'defaultSemiBold' ? styles . defaultSemiBold : undefined ,
type === 'subtitle' ? styles . subtitle : undefined ,
type === 'link' ? styles . link : undefined ,
style ,
] }
{ ... rest }
/>
);
}
const styles = StyleSheet . create ({
default: {
fontSize: 16 ,
lineHeight: 24 ,
},
defaultSemiBold: {
fontSize: 16 ,
lineHeight: 24 ,
fontWeight: '600' ,
},
title: {
fontSize: 32 ,
fontWeight: 'bold' ,
lineHeight: 32 ,
},
subtitle: {
fontSize: 20 ,
fontWeight: 'bold' ,
},
link: {
lineHeight: 30 ,
fontSize: 16 ,
color: '#0a7ea4' ,
},
});
default
title
subtitle
defaultSemiBold
link
Standard body text: < ThemedText > Regular paragraph text </ ThemedText >
Font size: 16px
Line height: 24px
Large heading text: < ThemedText type = "title" > Page Title </ ThemedText >
Font size: 32px
Font weight: bold
Section headings: < ThemedText type = "subtitle" > Section Title </ ThemedText >
Font size: 20px
Font weight: bold
Emphasized body text: < ThemedText type = "defaultSemiBold" > Important info </ ThemedText >
Font size: 16px
Font weight: 600
Interactive link text: < ThemedText type = "link" > Click here </ ThemedText >
Font size: 16px
Color: #0a7ea4 (fixed)
Using Fonts
Apply custom fonts from the Fonts constant:
import { ThemedText } from '@/components/themed-text' ;
import { Fonts } from '@/constants/theme' ;
function ExploreScreen () {
return (
< ThemedText
type = "title"
style = { { fontFamily: Fonts . rounded } }
>
Explore
</ ThemedText >
);
}
Theme Integration Example
Here’s how theming is integrated in the tab navigator:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router' ;
import { Colors } from '@/constants/theme' ;
import { useColorScheme } from '@/hooks/use-color-scheme' ;
import { IconSymbol } from '@/components/ui/icon-symbol' ;
export default function TabLayout () {
const colorScheme = useColorScheme ();
return (
< Tabs
screenOptions = { {
// Dynamically set active tint color
tabBarActiveTintColor: Colors [ colorScheme ?? 'light' ]. tint ,
headerShown: false ,
} } >
< Tabs.Screen
name = "index"
options = { {
title: 'Home' ,
tabBarIcon : ({ color }) => (
< IconSymbol size = { 28 } name = "house.fill" color = { color } />
),
} }
/>
</ Tabs >
);
}
The color prop passed to tabBarIcon automatically reflects the theme-aware color.
Creating Custom Themed Components
Follow this pattern to create your own themed components:
import { Button , type ButtonProps } from 'react-native' ;
import { useThemeColor } from '@/hooks/use-theme-color' ;
export type ThemedButtonProps = ButtonProps & {
lightColor ?: string ;
darkColor ?: string ;
};
export function ThemedButton ({
lightColor ,
darkColor ,
... props
} : ThemedButtonProps ) {
const color = useThemeColor (
{ light: lightColor , dark: darkColor },
'tint'
);
return < Button color = { color } { ... props } /> ;
}
Root Theme Provider
The root layout wraps the app with React Navigation’s ThemeProvider:
// app/_layout.tsx
import { DarkTheme , DefaultTheme , ThemeProvider } from '@react-navigation/native' ;
import { useColorScheme } from '@/hooks/use-color-scheme' ;
export default function RootLayout () {
const colorScheme = useColorScheme ();
return (
< ThemeProvider value = { colorScheme === 'dark' ? DarkTheme : DefaultTheme } >
{ /* Your app screens */ }
</ ThemeProvider >
);
}
This ensures navigation components (headers, tabs) also adapt to the theme.
Best Practices
Always Use Themed Components Prefer ThemedView and ThemedText over raw React Native components to ensure consistent theming.
Extend Color Palette Add new color keys to Colors when you need semantic colors like error, success, or warning.
Test Both Modes Always test your UI in both light and dark modes to ensure proper contrast and readability.
Respect System Preferences The theme automatically syncs with system preferences—avoid forcing a specific theme.
Advanced: Per-Component Color Overrides
You can override colors on a per-component basis:
< ThemedView
lightColor = "#ffffff"
darkColor = "#000000"
style = { { padding: 16 } }
>
< ThemedText
lightColor = "#333333"
darkColor = "#eeeeee"
>
Custom themed content
</ ThemedText >
</ ThemedView >
This is useful for special UI elements that need different colors than the default theme.
Troubleshooting
Theme not updating when system changes
Ensure you’re using useColorScheme from @/hooks/use-color-scheme, not directly from React Native in components. // ✅ Correct
import { useColorScheme } from '@/hooks/use-color-scheme' ;
// ❌ Incorrect
import { useColorScheme } from 'react-native' ;
Colors look wrong in one mode
Check that you’ve defined the color key in both Colors.light and Colors.dark. Missing keys will cause TypeScript errors.
Make sure you’re using Fonts from constants/theme.ts with proper platform detection. Some fonts require loading with expo-font first.
Next Steps