Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Styling & Theming

Module Overview

Estimated Time: 3 hours | Difficulty: Beginner-Intermediate | Prerequisites: Core Components
Styling in React Native looks like CSS at first glance — properties like padding, backgroundColor, and fontSize are familiar — but the similarities are deceiving. React Native uses a JavaScript-based styling system that compiles to native layout primitives (UIKit on iOS, Android Views on Android). There is no cascade, no CSS specificity, and no global stylesheets. Every style is scoped to its component, which eliminates an entire class of bugs but requires different organizational patterns. This module covers the StyleSheet API, responsive design, and building a comprehensive theming system that scales across your entire application. 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

StyleSheet.create is not just a convenience wrapper — it validates your styles at creation time and sends them across the bridge only once (rather than on every render), which improves performance. Think of it as pre-compiling your styles.

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

React Native supports composing styles by passing an array. Styles later in the array override earlier ones — like stacking transparency sheets on an overhead projector.
// Combining multiple styles -- later values win on conflict
<View style={[styles.base, styles.modifier, conditionalStyle && styles.active]} />

// Inline style overrides -- use sparingly, as inline objects are recreated
// on every render. Fine for one-off tweaks, but not for complex styling.
<View style={[styles.container, { marginTop: 20 }]} />

// Dynamic styles -- conditionally apply different values.
// For frequently-toggling styles, consider memoizing the style array.
<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

Design tokens are the atomic constants of your visual language — named values for colors, spacing, typography, and elevation that are used everywhere instead of raw hex codes and pixel values. They serve the same purpose as variables in a design tool like Figma: change the token once, and the entire app updates. The key insight is to create two layers: primitive tokens (the raw color palette, e.g., blue-500: '#3b82f6') and semantic tokens (the purpose, e.g., primary: colors.blue[500]). This separation lets you swap themes (light/dark) by remapping semantic tokens without touching primitives.

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

Mobile responsive design is different from web responsive design. On the web, you are dealing with viewport widths from 320px to 2560px+. On mobile, the range is narrower (320pt to about 430pt for phones, up to 1024pt for tablets), but you also have to handle orientation changes, dynamic type sizes, and the notch/Dynamic Island on modern iPhones. The approach: design for a base device (typically iPhone 13/14 at 390pt wide), then scale proportionally for smaller and larger screens.

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

iOS and Android have fundamentally different visual conventions. Shadows work differently (iOS uses shadowColor/shadowOffset/shadowOpacity/shadowRadius, Android uses elevation). Default fonts differ. Status bar padding varies. Platform.select lets you handle these differences inline without splitting into separate files.
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