Skip to main content
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:
  1. Color and font constants (constants/theme.ts)
  2. Theme detection hook (hooks/use-color-scheme.ts)
  3. 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

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',
  },
});
Standard body text:
<ThemedText>Regular paragraph text</ThemedText>
  • Font size: 16px
  • Line height: 24px

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

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';
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

Build docs developers (and LLMs) love