Skip to main content
Styling & Theming

Module Overview

Estimated Time: 3 hours | Difficulty: Beginner-Intermediate | Prerequisites: Core Components
Styling in React Native differs from web CSS but offers powerful capabilities for building beautiful, consistent UIs. This module covers StyleSheet API, responsive design, and building a comprehensive theming system. What You’ll Learn:
  • StyleSheet API deep dive
  • Responsive styling techniques
  • Dark/light theme implementation
  • Design tokens and theming
  • Platform-specific styles
  • Styled components patterns

StyleSheet Fundamentals

Creating Styles

import { StyleSheet, View, Text } from 'react-native';

function StyledComponent() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Hello World</Text>
      <Text style={[styles.text, styles.bold]}>Combined styles</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#ffffff',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 8,
  },
  text: {
    fontSize: 16,
    color: '#6b7280',
    lineHeight: 24,
  },
  bold: {
    fontWeight: '600',
  },
});

Style Composition

// Combining multiple styles
<View style={[styles.base, styles.modifier, conditionalStyle && styles.active]} />

// Inline style overrides
<View style={[styles.container, { marginTop: 20 }]} />

// Dynamic styles
<View style={[styles.box, { backgroundColor: isActive ? '#3b82f6' : '#e5e7eb' }]} />

StyleSheet.flatten

// Flatten combined styles for inspection
const flattenedStyle = StyleSheet.flatten([styles.base, styles.modifier]);
console.log(flattenedStyle); // { padding: 16, margin: 8, ... }

Design Tokens

Token System

// src/theme/tokens.ts
export const colors = {
  // Brand colors
  primary: {
    50: '#eff6ff',
    100: '#dbeafe',
    200: '#bfdbfe',
    300: '#93c5fd',
    400: '#60a5fa',
    500: '#3b82f6',
    600: '#2563eb',
    700: '#1d4ed8',
    800: '#1e40af',
    900: '#1e3a8a',
  },
  
  // Neutral colors
  gray: {
    50: '#f9fafb',
    100: '#f3f4f6',
    200: '#e5e7eb',
    300: '#d1d5db',
    400: '#9ca3af',
    500: '#6b7280',
    600: '#4b5563',
    700: '#374151',
    800: '#1f2937',
    900: '#111827',
  },
  
  // Semantic colors
  success: '#22c55e',
  warning: '#f59e0b',
  error: '#ef4444',
  info: '#3b82f6',
  
  // Base colors
  white: '#ffffff',
  black: '#000000',
  transparent: 'transparent',
} as const;

export const spacing = {
  0: 0,
  1: 4,
  2: 8,
  3: 12,
  4: 16,
  5: 20,
  6: 24,
  8: 32,
  10: 40,
  12: 48,
  16: 64,
  20: 80,
  24: 96,
} as const;

export const typography = {
  fontFamily: {
    regular: 'Inter-Regular',
    medium: 'Inter-Medium',
    semibold: 'Inter-SemiBold',
    bold: 'Inter-Bold',
  },
  fontSize: {
    xs: 12,
    sm: 14,
    base: 16,
    lg: 18,
    xl: 20,
    '2xl': 24,
    '3xl': 30,
    '4xl': 36,
  },
  lineHeight: {
    tight: 1.25,
    normal: 1.5,
    relaxed: 1.75,
  },
} as const;

export const borderRadius = {
  none: 0,
  sm: 4,
  md: 8,
  lg: 12,
  xl: 16,
  '2xl': 24,
  full: 9999,
} as const;

export const shadows = {
  sm: {
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.05,
    shadowRadius: 2,
    elevation: 1,
  },
  md: {
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.1,
    shadowRadius: 6,
    elevation: 3,
  },
  lg: {
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 10 },
    shadowOpacity: 0.15,
    shadowRadius: 15,
    elevation: 5,
  },
} as const;

Theme System

Theme Definition

// src/theme/themes.ts
import { colors, spacing, typography, borderRadius, shadows } from './tokens';

export interface Theme {
  colors: {
    background: string;
    surface: string;
    surfaceVariant: string;
    primary: string;
    primaryContainer: string;
    secondary: string;
    text: string;
    textSecondary: string;
    textTertiary: string;
    border: string;
    divider: string;
    error: string;
    success: string;
    warning: string;
  };
  spacing: typeof spacing;
  typography: typeof typography;
  borderRadius: typeof borderRadius;
  shadows: typeof shadows;
}

export const lightTheme: Theme = {
  colors: {
    background: colors.white,
    surface: colors.white,
    surfaceVariant: colors.gray[50],
    primary: colors.primary[600],
    primaryContainer: colors.primary[100],
    secondary: colors.gray[600],
    text: colors.gray[900],
    textSecondary: colors.gray[600],
    textTertiary: colors.gray[400],
    border: colors.gray[200],
    divider: colors.gray[100],
    error: colors.error,
    success: colors.success,
    warning: colors.warning,
  },
  spacing,
  typography,
  borderRadius,
  shadows,
};

export const darkTheme: Theme = {
  colors: {
    background: colors.gray[900],
    surface: colors.gray[800],
    surfaceVariant: colors.gray[700],
    primary: colors.primary[400],
    primaryContainer: colors.primary[900],
    secondary: colors.gray[400],
    text: colors.gray[50],
    textSecondary: colors.gray[300],
    textTertiary: colors.gray[500],
    border: colors.gray[700],
    divider: colors.gray[800],
    error: '#f87171',
    success: '#4ade80',
    warning: '#fbbf24',
  },
  spacing,
  typography,
  borderRadius,
  shadows,
};

Theme Context

// src/theme/ThemeContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { useColorScheme } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Theme, lightTheme, darkTheme } from './themes';

type ThemeMode = 'light' | 'dark' | 'system';

interface ThemeContextType {
  theme: Theme;
  themeMode: ThemeMode;
  isDark: boolean;
  setThemeMode: (mode: ThemeMode) => void;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

const THEME_STORAGE_KEY = 'app_theme_mode';

export function ThemeProvider({ children }: { children: ReactNode }) {
  const systemColorScheme = useColorScheme();
  const [themeMode, setThemeModeState] = useState<ThemeMode>('system');

  // Determine if dark mode is active
  const isDark = themeMode === 'system' 
    ? systemColorScheme === 'dark' 
    : themeMode === 'dark';

  const theme = isDark ? darkTheme : lightTheme;

  // Load saved theme preference
  useEffect(() => {
    AsyncStorage.getItem(THEME_STORAGE_KEY).then((savedMode) => {
      if (savedMode && ['light', 'dark', 'system'].includes(savedMode)) {
        setThemeModeState(savedMode as ThemeMode);
      }
    });
  }, []);

  const setThemeMode = async (mode: ThemeMode) => {
    setThemeModeState(mode);
    await AsyncStorage.setItem(THEME_STORAGE_KEY, mode);
  };

  const toggleTheme = () => {
    const newMode = isDark ? 'light' : 'dark';
    setThemeMode(newMode);
  };

  return (
    <ThemeContext.Provider value={{ theme, themeMode, isDark, setThemeMode, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

Using Theme in Components

// src/components/ui/Card.tsx
import { View, StyleSheet, ViewStyle } from 'react-native';
import { useTheme } from '@/theme/ThemeContext';

interface CardProps {
  children: React.ReactNode;
  variant?: 'elevated' | 'outlined' | 'filled';
  style?: ViewStyle;
}

export function Card({ children, variant = 'elevated', style }: CardProps) {
  const { theme } = useTheme();

  const variantStyles: Record<string, ViewStyle> = {
    elevated: {
      backgroundColor: theme.colors.surface,
      ...theme.shadows.md,
    },
    outlined: {
      backgroundColor: theme.colors.surface,
      borderWidth: 1,
      borderColor: theme.colors.border,
    },
    filled: {
      backgroundColor: theme.colors.surfaceVariant,
    },
  };

  return (
    <View
      style={[
        styles.card,
        { borderRadius: theme.borderRadius.lg },
        variantStyles[variant],
        style,
      ]}
    >
      {children}
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    padding: 16,
  },
});

Responsive Design

Dimensions Hook

// src/hooks/useDimensions.ts
import { useState, useEffect } from 'react';
import { Dimensions, ScaledSize } from 'react-native';

interface DimensionsState {
  window: ScaledSize;
  screen: ScaledSize;
  isPortrait: boolean;
  isLandscape: boolean;
  isSmallDevice: boolean;
  isMediumDevice: boolean;
  isLargeDevice: boolean;
}

export function useDimensions(): DimensionsState {
  const [dimensions, setDimensions] = useState(() => ({
    window: Dimensions.get('window'),
    screen: Dimensions.get('screen'),
  }));

  useEffect(() => {
    const subscription = Dimensions.addEventListener('change', ({ window, screen }) => {
      setDimensions({ window, screen });
    });

    return () => subscription.remove();
  }, []);

  const { width, height } = dimensions.window;

  return {
    ...dimensions,
    isPortrait: height > width,
    isLandscape: width > height,
    isSmallDevice: width < 375,
    isMediumDevice: width >= 375 && width < 768,
    isLargeDevice: width >= 768,
  };
}

Responsive Styles

// src/utils/responsive.ts
import { Dimensions, PixelRatio } from 'react-native';

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

// Base dimensions (design reference)
const BASE_WIDTH = 375;
const BASE_HEIGHT = 812;

// Scale based on screen width
export function wp(widthPercent: number): number {
  return PixelRatio.roundToNearestPixel((SCREEN_WIDTH * widthPercent) / 100);
}

// Scale based on screen height
export function hp(heightPercent: number): number {
  return PixelRatio.roundToNearestPixel((SCREEN_HEIGHT * heightPercent) / 100);
}

// Scale font size
export function fontSize(size: number): number {
  const scale = SCREEN_WIDTH / BASE_WIDTH;
  const newSize = size * scale;
  return Math.round(PixelRatio.roundToNearestPixel(newSize));
}

// Scale spacing
export function scale(size: number): number {
  const scale = SCREEN_WIDTH / BASE_WIDTH;
  return Math.round(PixelRatio.roundToNearestPixel(size * scale));
}

// Vertical scale
export function verticalScale(size: number): number {
  const scale = SCREEN_HEIGHT / BASE_HEIGHT;
  return Math.round(PixelRatio.roundToNearestPixel(size * scale));
}

// Moderate scale (less aggressive)
export function moderateScale(size: number, factor: number = 0.5): number {
  return size + (scale(size) - size) * factor;
}

Breakpoint-Based Styles

// src/hooks/useBreakpoint.ts
import { useDimensions } from './useDimensions';

type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

const breakpoints = {
  xs: 0,
  sm: 375,
  md: 768,
  lg: 1024,
  xl: 1280,
};

export function useBreakpoint(): Breakpoint {
  const { window } = useDimensions();
  const width = window.width;

  if (width >= breakpoints.xl) return 'xl';
  if (width >= breakpoints.lg) return 'lg';
  if (width >= breakpoints.md) return 'md';
  if (width >= breakpoints.sm) return 'sm';
  return 'xs';
}

export function useResponsiveValue<T>(values: Partial<Record<Breakpoint, T>>): T | undefined {
  const breakpoint = useBreakpoint();
  const breakpointOrder: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl'];
  
  // Find the closest defined value
  const currentIndex = breakpointOrder.indexOf(breakpoint);
  for (let i = currentIndex; i >= 0; i--) {
    const bp = breakpointOrder[i];
    if (values[bp] !== undefined) {
      return values[bp];
    }
  }
  
  return undefined;
}

// Usage
function ResponsiveComponent() {
  const columns = useResponsiveValue({ xs: 1, sm: 2, md: 3, lg: 4 });
  const padding = useResponsiveValue({ xs: 16, md: 24, lg: 32 });
  
  return (
    <View style={{ padding }}>
      <FlatList numColumns={columns} ... />
    </View>
  );
}

Platform-Specific Styles

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 4,
      },
      android: {
        elevation: 4,
      },
    }),
  },
  
  text: {
    fontFamily: Platform.OS === 'ios' ? 'Helvetica' : 'Roboto',
    fontSize: 16,
  },
  
  // Platform-specific padding for safe areas
  header: {
    paddingTop: Platform.OS === 'ios' ? 44 : 0,
  },
});

// Platform-specific files
// Button.ios.tsx
// Button.android.tsx
// Button.tsx (fallback)

Styled Components Pattern

// src/components/ui/styled.tsx
import { View, Text, Pressable, ViewStyle, TextStyle, PressableProps } from 'react-native';
import { useTheme } from '@/theme/ThemeContext';

// Box component with theme-aware styling
interface BoxProps {
  children?: React.ReactNode;
  p?: number;
  px?: number;
  py?: number;
  m?: number;
  mx?: number;
  my?: number;
  bg?: string;
  rounded?: keyof typeof import('@/theme/tokens').borderRadius;
  shadow?: keyof typeof import('@/theme/tokens').shadows;
  style?: ViewStyle;
}

export function Box({
  children,
  p,
  px,
  py,
  m,
  mx,
  my,
  bg,
  rounded,
  shadow,
  style,
}: BoxProps) {
  const { theme } = useTheme();

  const boxStyle: ViewStyle = {
    ...(p !== undefined && { padding: theme.spacing[p as keyof typeof theme.spacing] }),
    ...(px !== undefined && { paddingHorizontal: theme.spacing[px as keyof typeof theme.spacing] }),
    ...(py !== undefined && { paddingVertical: theme.spacing[py as keyof typeof theme.spacing] }),
    ...(m !== undefined && { margin: theme.spacing[m as keyof typeof theme.spacing] }),
    ...(mx !== undefined && { marginHorizontal: theme.spacing[mx as keyof typeof theme.spacing] }),
    ...(my !== undefined && { marginVertical: theme.spacing[my as keyof typeof theme.spacing] }),
    ...(bg && { backgroundColor: bg }),
    ...(rounded && { borderRadius: theme.borderRadius[rounded] }),
    ...(shadow && theme.shadows[shadow]),
  };

  return <View style={[boxStyle, style]}>{children}</View>;
}

// Typography component
interface TypographyProps {
  children: React.ReactNode;
  variant?: 'h1' | 'h2' | 'h3' | 'body' | 'caption';
  color?: string;
  align?: TextStyle['textAlign'];
  style?: TextStyle;
}

export function Typography({
  children,
  variant = 'body',
  color,
  align,
  style,
}: TypographyProps) {
  const { theme } = useTheme();

  const variantStyles: Record<string, TextStyle> = {
    h1: {
      fontSize: theme.typography.fontSize['3xl'],
      fontFamily: theme.typography.fontFamily.bold,
      lineHeight: theme.typography.fontSize['3xl'] * theme.typography.lineHeight.tight,
    },
    h2: {
      fontSize: theme.typography.fontSize['2xl'],
      fontFamily: theme.typography.fontFamily.semibold,
      lineHeight: theme.typography.fontSize['2xl'] * theme.typography.lineHeight.tight,
    },
    h3: {
      fontSize: theme.typography.fontSize.xl,
      fontFamily: theme.typography.fontFamily.semibold,
      lineHeight: theme.typography.fontSize.xl * theme.typography.lineHeight.normal,
    },
    body: {
      fontSize: theme.typography.fontSize.base,
      fontFamily: theme.typography.fontFamily.regular,
      lineHeight: theme.typography.fontSize.base * theme.typography.lineHeight.normal,
    },
    caption: {
      fontSize: theme.typography.fontSize.sm,
      fontFamily: theme.typography.fontFamily.regular,
      lineHeight: theme.typography.fontSize.sm * theme.typography.lineHeight.normal,
    },
  };

  return (
    <Text
      style={[
        variantStyles[variant],
        { color: color || theme.colors.text },
        align && { textAlign: align },
        style,
      ]}
    >
      {children}
    </Text>
  );
}

Theme Switcher Component

// src/components/ThemeSwitcher.tsx
import { View, Pressable, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
  useAnimatedStyle,
  withSpring,
  interpolateColor,
} from 'react-native-reanimated';
import { useTheme } from '@/theme/ThemeContext';

export function ThemeSwitcher() {
  const { theme, isDark, toggleTheme } = useTheme();

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: withSpring(isDark ? 28 : 0) }],
  }));

  const trackStyle = useAnimatedStyle(() => ({
    backgroundColor: interpolateColor(
      isDark ? 1 : 0,
      [0, 1],
      [theme.colors.border, theme.colors.primary]
    ),
  }));

  return (
    <Pressable onPress={toggleTheme}>
      <Animated.View style={[styles.track, trackStyle]}>
        <Animated.View style={[styles.thumb, animatedStyle]}>
          <Ionicons
            name={isDark ? 'moon' : 'sunny'}
            size={16}
            color={isDark ? '#fbbf24' : '#f59e0b'}
          />
        </Animated.View>
      </Animated.View>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  track: {
    width: 56,
    height: 28,
    borderRadius: 14,
    padding: 2,
  },
  thumb: {
    width: 24,
    height: 24,
    borderRadius: 12,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
  },
});

Best Practices

Use StyleSheet.create

Always use StyleSheet.create for performance optimization

Avoid Inline Styles

Minimize inline styles to prevent unnecessary re-renders

Design Tokens

Use design tokens for consistent spacing, colors, and typography

Theme Context

Implement theme context for easy dark/light mode switching

Next Steps

Module 7: Flexbox Mastery

Master Flexbox layout for building complex, responsive UIs