Module Overview
Estimated Time: 3 hours | Difficulty: Beginner-Intermediate | Prerequisites: Core Components
- 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
Copy
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
Copy
// 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
Copy
// Flatten combined styles for inspection
const flattenedStyle = StyleSheet.flatten([styles.base, styles.modifier]);
console.log(flattenedStyle); // { padding: 16, margin: 8, ... }
Design Tokens
Token System
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
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
Copy
// 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
Copy
// 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