Common Layout Patterns
Production-ready layouts you'll use in every app
Table of Contents
🎯 Learning Objectives
- Build reusable screen scaffold layouts for consistent app structure
- Create header variations for different navigation needs
- Design card components that adapt to different content types
- Implement list items for settings, contacts, and media content
- Structure form layouts that work well on mobile
- Build responsive grids for galleries and dashboards
- Create overlay and modal patterns for dialogs and menus
Introduction
Now that you understand Flexbox fundamentals, it's time to see how these concepts combine into real-world layouts. Every mobile app is built from a relatively small set of layout patterns — headers, cards, lists, forms, grids, and modals. Master these patterns, and you can build virtually any screen.
This lesson is your pattern library. Each pattern is production-ready code you can adapt for your projects. We'll focus on the structural layout, keeping styling minimal so you can clearly see the flex mechanics at work.
📱 The 80/20 of Mobile Layouts
About 80% of mobile screens use combinations of just these patterns: a screen container with header/content/footer, horizontal rows for navigation, vertical stacks for content, cards for grouped information, and list items for repeating data. Learn these well, and layout becomes second nature.
How to Use This Lesson
Each pattern includes a visual representation, complete code, and notes on when to use it. Think of this as a reference you'll return to. When building a new screen, ask: "Which patterns do I need here?" Then combine them.
flowchart LR
subgraph Patterns["Layout Pattern Library"]
S["Screen Scaffolds"]
H["Headers"]
C["Cards"]
L["List Items"]
F["Forms"]
G["Grids"]
O["Overlays"]
end
Patterns --> Combine["Combine & Customize"]
Combine --> Screen["Your App Screen"]
style Patterns fill:#e3f2fd
style Combine fill:#fff3e0
style Screen fill:#e8f5e9
Screen Scaffolds
A screen scaffold is the outermost container that structures your entire screen. These patterns handle the relationship between headers, content areas, and footers.
Pattern: Basic Screen
The simplest scaffold — a full-screen container for your content.
import { View, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
function BasicScreen({ children }) {
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.screen}>
{children}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
screen: {
flex: 1,
padding: 16,
},
});
Pattern: Screen with Fixed Header
Most screens need a header. This pattern keeps the header fixed while content scrolls.
function ScreenWithHeader({ children, title }) {
return (
<SafeAreaView style={styles.safeArea}>
{/* Fixed Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>{title}</Text>
</View>
{/* Scrollable Content */}
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
>
{children}
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
header: {
height: 56,
justifyContent: 'center',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
},
content: {
flex: 1,
},
contentContainer: {
padding: 16,
},
});
Pattern: Screen with Header and Footer
Common for checkout flows, forms with submit buttons, or any screen needing persistent bottom actions.
function ScreenWithHeaderFooter({ children, title, onSubmit }) {
return (
<SafeAreaView style={styles.safeArea}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>{title}</Text>
</View>
{/* Scrollable Content */}
<ScrollView style={styles.content}>
{children}
</ScrollView>
{/* Fixed Footer */}
<View style={styles.footer}>
<Pressable style={styles.submitButton} onPress={onSubmit}>
<Text style={styles.submitText}>Submit</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
header: {
height: 56,
justifyContent: 'center',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
},
content: {
flex: 1, // Takes all space between header and footer
},
footer: {
padding: 16,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
submitButton: {
backgroundColor: '#4CAF50',
paddingVertical: 14,
borderRadius: 8,
alignItems: 'center',
},
submitText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
Pattern: Tab Screen with Bottom Navigation
The standard pattern for apps with bottom tab navigation.
function TabScreen({ children, activeTab, onTabPress }) {
return (
<SafeAreaView style={styles.safeArea}>
{/* Main Content */}
<View style={styles.content}>
{children}
</View>
{/* Bottom Tab Bar */}
<View style={styles.tabBar}>
{['home', 'search', 'profile'].map((tab) => (
<Pressable
key={tab}
style={styles.tab}
onPress={() => onTabPress(tab)}
>
<Text style={styles.tabIcon}>
{tab === 'home' ? '🏠' : tab === 'search' ? '🔍' : '👤'}
</Text>
{activeTab === tab && <View style={styles.activeIndicator} />}
</Pressable>
))}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
content: {
flex: 1,
},
tabBar: {
flexDirection: 'row',
height: 56,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
tab: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
tabIcon: {
fontSize: 24,
},
activeIndicator: {
position: 'absolute',
bottom: 8,
width: 24,
height: 3,
backgroundColor: '#2196F3',
borderRadius: 2,
},
});
💡 When to Use Each Scaffold
- Basic Screen: Simple content pages, detail views
- Header + Content: Most screens in your app
- Header + Content + Footer: Forms, checkout, multi-step flows
- Tab Screen: Main navigation shell (usually provided by navigation library)
Header Patterns
Headers need to accommodate various combinations of navigation, titles, and actions. Here are the patterns that cover most needs.
Pattern: Simple Centered Title
function SimpleHeader({ title }) {
return (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
header: {
height: 56,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#1a1a1a',
},
});
Pattern: Back Button + Title
function HeaderWithBack({ title, onBack }) {
return (
<View style={styles.header}>
<Pressable style={styles.backButton} onPress={onBack}>
<Text style={styles.backIcon}>←</Text>
</Pressable>
<Text style={styles.title}>{title}</Text>
{/* Empty view for balance */}
<View style={styles.placeholder} />
</View>
);
}
const styles = StyleSheet.create({
header: {
height: 56,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
backButton: {
width: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
backIcon: {
fontSize: 24,
color: '#2196F3',
},
title: {
flex: 1,
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
color: '#1a1a1a',
},
placeholder: {
width: 44, // Same as back button for centering
},
});
Pattern: Back + Title + Actions
function HeaderWithActions({ title, onBack, onSave }) {
return (
<View style={styles.header}>
<Pressable style={styles.button} onPress={onBack}>
<Text style={styles.backIcon}>←</Text>
</Pressable>
<Text style={styles.title}>{title}</Text>
<Pressable style={styles.button} onPress={onSave}>
<Text style={styles.actionText}>Save</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
header: {
height: 56,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
button: {
minWidth: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 8,
},
backIcon: {
fontSize: 24,
color: '#2196F3',
},
title: {
flex: 1,
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
color: '#1a1a1a',
},
actionText: {
fontSize: 16,
fontWeight: '600',
color: '#2196F3',
},
});
Pattern: Search Header
function SearchHeader({ value, onChangeText, onCancel }) {
return (
<View style={styles.header}>
<View style={styles.searchContainer}>
<Text style={styles.searchIcon}>🔍</Text>
<TextInput
style={styles.searchInput}
placeholder="Search..."
value={value}
onChangeText={onChangeText}
autoFocus
/>
</View>
<Pressable style={styles.cancelButton} onPress={onCancel}>
<Text style={styles.cancelText}>Cancel</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
header: {
height: 56,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
backgroundColor: '#fff',
gap: 12,
},
searchContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f5f5f5',
borderRadius: 20,
paddingHorizontal: 12,
height: 40,
},
searchIcon: {
fontSize: 16,
marginRight: 8,
},
searchInput: {
flex: 1,
fontSize: 16,
color: '#1a1a1a',
},
cancelButton: {
paddingVertical: 8,
},
cancelText: {
fontSize: 16,
color: '#2196F3',
},
});
Pattern: Logo + Actions Header
function LogoHeader({ onNotifications, onSettings }) {
return (
<View style={styles.header}>
{/* Logo area */}
<View style={styles.logoContainer}>
<View style={styles.logoIcon}>
<Text style={styles.logoLetter}>A</Text>
</View>
<Text style={styles.logoText}>AppName</Text>
</View>
{/* Spacer */}
<View style={styles.spacer} />
{/* Action buttons */}
<View style={styles.actions}>
<Pressable style={styles.iconButton} onPress={onNotifications}>
<Text style={styles.icon}>🔔</Text>
</Pressable>
<Pressable style={styles.iconButton} onPress={onSettings}>
<Text style={styles.icon}>⚙️</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
header: {
height: 56,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
logoContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
logoIcon: {
width: 32,
height: 32,
backgroundColor: '#2196F3',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
logoLetter: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
logoText: {
fontSize: 18,
fontWeight: '700',
color: '#1a1a1a',
},
spacer: {
flex: 1,
},
actions: {
flexDirection: 'row',
gap: 4,
},
iconButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
icon: {
fontSize: 22,
},
});
Card Patterns
Cards are self-contained units of content. They group related information and provide a visual boundary. Here are the most common card layouts.
Pattern: Basic Content Card
function ContentCard({ title, description, onPress }) {
return (
<Pressable style={styles.card} onPress={onPress}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
<Text style={styles.link}>Learn More →</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
// iOS Shadow
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
// Android Shadow
elevation: 3,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
description: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 12,
},
link: {
fontSize: 14,
fontWeight: '600',
color: '#2196F3',
},
});
Pattern: Horizontal Card (Image + Content)
function HorizontalCard({ image, title, subtitle, price }) {
return (
<View style={styles.card}>
<Image source={{ uri: image }} style={styles.image} />
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
<Text style={styles.price}>{price}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 12,
padding: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
image: {
width: 80,
height: 80,
borderRadius: 8,
backgroundColor: '#f0f0f0',
},
content: {
flex: 1,
marginLeft: 12,
justifyContent: 'center',
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
},
subtitle: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
price: {
fontSize: 16,
fontWeight: '700',
color: '#4CAF50',
marginTop: 4,
},
});
Pattern: Vertical Card (Image Top)
function VerticalCard({ image, title, subtitle, location, rating }) {
return (
<View style={styles.card}>
<Image source={{ uri: image }} style={styles.image} />
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
<Text style={styles.location}>📍 {location}</Text>
<View style={styles.ratingRow}>
<Text style={styles.stars}>{'★'.repeat(Math.floor(rating))}{'☆'.repeat(5 - Math.floor(rating))}</Text>
<Text style={styles.ratingText}>({rating})</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden', // Clips image to border radius
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
image: {
width: '100%',
height: 140,
backgroundColor: '#f0f0f0',
},
content: {
padding: 12,
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
},
subtitle: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
location: {
fontSize: 12,
color: '#999',
marginTop: 8,
},
ratingRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
gap: 4,
},
stars: {
color: '#ffc107',
fontSize: 14,
},
ratingText: {
fontSize: 12,
color: '#666',
},
});
Pattern: Stats Card
function StatsCard({ stats }) {
return (
<View style={styles.card}>
{stats.map((stat, index) => (
<React.Fragment key={stat.label}>
{index > 0 && <View style={styles.divider} />}
<View style={styles.stat}>
<Text style={styles.statValue}>{stat.value}</Text>
<Text style={styles.statLabel}>{stat.label}</Text>
</View>
</React.Fragment>
))}
</View>
);
}
// Usage:
// <StatsCard stats={[
// { value: '1,234', label: 'Followers' },
// { value: '567', label: 'Following' },
// { value: '89', label: 'Posts' },
// ]} />
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
stat: {
flex: 1,
alignItems: 'center',
},
statValue: {
fontSize: 24,
fontWeight: '700',
color: '#1a1a1a',
},
statLabel: {
fontSize: 13,
color: '#666',
marginTop: 4,
},
divider: {
width: 1,
backgroundColor: '#e0e0e0',
marginHorizontal: 8,
},
});
List Item Patterns
List items are the building blocks of scrollable content. Whether you're building a settings screen, contact list, or feed, these patterns have you covered.
Pattern: Simple List Item
function SimpleListItem({ title, onPress }) {
return (
<Pressable style={styles.item} onPress={onPress}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.chevron}>›</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 16,
color: '#1a1a1a',
},
chevron: {
fontSize: 20,
color: '#999',
},
});
Pattern: List Item with Icon
function IconListItem({ icon, title, onPress }) {
return (
<Pressable style={styles.item} onPress={onPress}>
<View style={styles.iconContainer}>
<Text style={styles.icon}>{icon}</Text>
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.chevron}>›</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
iconContainer: {
width: 36,
height: 36,
backgroundColor: '#e3f2fd',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
icon: {
fontSize: 18,
},
title: {
flex: 1,
fontSize: 16,
color: '#1a1a1a',
},
chevron: {
fontSize: 20,
color: '#999',
},
});
Pattern: Contact/User List Item
function ContactListItem({ name, subtitle, avatarColor, isOnline, onPress }) {
const initials = name.split(' ').map(n => n[0]).join('').slice(0, 2);
return (
<Pressable style={styles.item} onPress={onPress}>
<View style={[styles.avatar, { backgroundColor: avatarColor }]}>
<Text style={styles.initials}>{initials}</Text>
</View>
<View style={styles.textContainer}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
{isOnline && <View style={styles.onlineIndicator} />}
</Pressable>
);
}
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
avatar: {
width: 48,
height: 48,
borderRadius: 24,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
initials: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
textContainer: {
flex: 1,
},
name: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
},
subtitle: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
onlineIndicator: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: '#4CAF50',
},
});
Pattern: Settings Item with Toggle
import { Switch } from 'react-native';
function SettingsToggleItem({ icon, title, subtitle, value, onValueChange }) {
return (
<View style={styles.item}>
<Text style={styles.icon}>{icon}</Text>
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
</View>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: '#e0e0e0', true: '#81c784' }}
thumbColor={value ? '#4CAF50' : '#f5f5f5'}
/>
</View>
);
}
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
icon: {
fontSize: 20,
marginRight: 12,
},
textContainer: {
flex: 1,
},
title: {
fontSize: 16,
color: '#1a1a1a',
},
subtitle: {
fontSize: 13,
color: '#666',
marginTop: 2,
},
});
Form Layouts
Forms require careful layout to be usable on mobile. These patterns ensure good touch targets and clear visual hierarchy.
Pattern: Labeled Input
function LabeledInput({ label, placeholder, value, onChangeText, error }) {
return (
<View style={styles.inputGroup}>
<Text style={styles.label}>{label}</Text>
<TextInput
style={[styles.input, error && styles.inputError]}
placeholder={placeholder}
value={value}
onChangeText={onChangeText}
placeholderTextColor="#999"
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
}
const styles = StyleSheet.create({
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
input: {
height: 48,
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 8,
paddingHorizontal: 16,
fontSize: 16,
color: '#1a1a1a',
backgroundColor: '#fff',
},
inputError: {
borderColor: '#f44336',
},
errorText: {
fontSize: 12,
color: '#f44336',
marginTop: 4,
},
});
Pattern: Form Section with Header
function FormSection({ title, children }) {
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.sectionContent}>
{children}
</View>
</View>
);
}
const styles = StyleSheet.create({
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#666',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 12,
paddingHorizontal: 16,
},
sectionContent: {
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden',
},
});
Pattern: Complete Login Form
function LoginForm({ onLogin }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<View style={styles.form}>
{/* Email Input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
placeholder="your@email.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
/>
</View>
{/* Password Input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
placeholder="Enter password"
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="password"
/>
</View>
{/* Forgot Password */}
<Pressable style={styles.forgotButton}>
<Text style={styles.forgotText}>Forgot Password?</Text>
</Pressable>
{/* Submit Button */}
<Pressable style={styles.submitButton} onPress={onLogin}>
<Text style={styles.submitText}>Log In</Text>
</Pressable>
{/* Divider */}
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or</Text>
<View style={styles.dividerLine} />
</View>
{/* Social Login */}
<View style={styles.socialButtons}>
<Pressable style={styles.socialButton}>
<Text>🍎 Apple</Text>
</Pressable>
<Pressable style={styles.socialButton}>
<Text>🔵 Google</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
form: {
padding: 24,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
input: {
height: 50,
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 10,
paddingHorizontal: 16,
fontSize: 16,
backgroundColor: '#fff',
},
forgotButton: {
alignSelf: 'flex-end',
marginBottom: 24,
},
forgotText: {
color: '#2196F3',
fontSize: 14,
},
submitButton: {
height: 50,
backgroundColor: '#2196F3',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
submitText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 24,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: '#e0e0e0',
},
dividerText: {
marginHorizontal: 16,
color: '#666',
fontSize: 14,
},
socialButtons: {
flexDirection: 'row',
gap: 12,
},
socialButton: {
flex: 1,
height: 50,
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
});
Grid Layouts
While React Native doesn't have CSS Grid, you can create grid layouts using Flexbox with flexWrap. These patterns are essential for galleries, dashboards, and icon menus.
Pattern: 2-Column Grid
function TwoColumnGrid({ items, renderItem }) {
return (
<View style={styles.grid}>
{items.map((item, index) => (
<View key={index} style={styles.gridItem}>
{renderItem(item, index)}
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
padding: 12,
},
gridItem: {
width: '48%', // Slightly less than 50% to account for gap
// Or use exact calculation:
// width: (screenWidth - 36) / 2, // 12px padding * 2 + 12px gap
},
});
Pattern: 3-Column Icon Grid
function IconGrid({ items }) {
return (
<View style={styles.grid}>
{items.map((item) => (
<Pressable key={item.id} style={styles.iconItem} onPress={item.onPress}>
<View style={styles.iconContainer}>
<Text style={styles.icon}>{item.icon}</Text>
</View>
<Text style={styles.iconLabel}>{item.label}</Text>
</Pressable>
))}
</View>
);
}
// Usage:
// <IconGrid items={[
// { id: '1', icon: '📷', label: 'Camera', onPress: () => {} },
// { id: '2', icon: '🎵', label: 'Music', onPress: () => {} },
// { id: '3', icon: '📁', label: 'Files', onPress: () => {} },
// ]} />
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
padding: 16,
},
iconItem: {
width: '30%',
alignItems: 'center',
marginBottom: 20,
},
iconContainer: {
width: 64,
height: 64,
backgroundColor: '#f5f5f5',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
icon: {
fontSize: 28,
},
iconLabel: {
fontSize: 12,
color: '#666',
textAlign: 'center',
},
});
Pattern: Photo Gallery Grid
function PhotoGallery({ photos, numColumns = 3 }) {
const { width } = useWindowDimensions();
const gap = 4;
const itemSize = (width - gap * (numColumns + 1)) / numColumns;
return (
<View style={styles.gallery}>
{photos.map((photo, index) => (
<Pressable
key={photo.id}
style={[styles.photoItem, { width: itemSize, height: itemSize }]}
>
<Image
source={{ uri: photo.url }}
style={styles.photo}
resizeMode="cover"
/>
</Pressable>
))}
</View>
);
}
const styles = StyleSheet.create({
gallery: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 4,
padding: 4,
},
photoItem: {
// width and height set dynamically
},
photo: {
width: '100%',
height: '100%',
},
});
Pattern: Dashboard Stats Grid
function DashboardGrid({ stats }) {
return (
<View style={styles.dashboard}>
{stats.map((stat) => (
<View key={stat.id} style={styles.statCard}>
<Text style={styles.statIcon}>{stat.icon}</Text>
<Text style={styles.statValue}>{stat.value}</Text>
<Text style={styles.statLabel}>{stat.label}</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
dashboard: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
padding: 16,
},
statCard: {
width: '47%',
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
statIcon: {
fontSize: 32,
marginBottom: 8,
},
statValue: {
fontSize: 28,
fontWeight: '700',
color: '#1a1a1a',
},
statLabel: {
fontSize: 13,
color: '#666',
marginTop: 4,
},
});
Overlay & Modal Patterns
Overlays and modals require careful positioning. These patterns create professional dialogs, bottom sheets, and action menus.
Pattern: Centered Modal
function CenteredModal({ visible, title, message, onCancel, onConfirm }) {
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={styles.modal}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>{message}</Text>
<View style={styles.buttonRow}>
<Pressable style={styles.cancelButton} onPress={onCancel}>
<Text style={styles.cancelText}>Cancel</Text>
</Pressable>
<Pressable style={styles.confirmButton} onPress={onConfirm}>
<Text style={styles.confirmText}>Confirm</Text>
</Pressable>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
modal: {
width: '100%',
maxWidth: 320,
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#1a1a1a',
textAlign: 'center',
marginBottom: 12,
},
message: {
fontSize: 15,
color: '#666',
textAlign: 'center',
lineHeight: 22,
marginBottom: 24,
},
buttonRow: {
flexDirection: 'row',
gap: 12,
},
cancelButton: {
flex: 1,
height: 44,
backgroundColor: '#f5f5f5',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
cancelText: {
fontSize: 15,
fontWeight: '600',
color: '#666',
},
confirmButton: {
flex: 1,
height: 44,
backgroundColor: '#2196F3',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
confirmText: {
fontSize: 15,
fontWeight: '600',
color: '#fff',
},
});
Pattern: Bottom Sheet
function BottomSheet({ visible, options, onSelect, onClose }) {
if (!visible) return null;
return (
<Pressable style={styles.overlay} onPress={onClose}>
<Pressable style={styles.sheet}>
{/* Handle */}
<View style={styles.handle} />
{/* Options */}
{options.map((option) => (
<Pressable
key={option.id}
style={styles.option}
onPress={() => onSelect(option)}
>
<Text style={styles.optionIcon}>{option.icon}</Text>
<Text style={[
styles.optionText,
option.destructive && styles.destructiveText,
]}>
{option.label}
</Text>
</Pressable>
))}
</Pressable>
</Pressable>
);
}
// Usage:
// <BottomSheet
// visible={showSheet}
// options={[
// { id: '1', icon: '📷', label: 'Take Photo' },
// { id: '2', icon: '🖼️', label: 'Choose from Library' },
// { id: '3', icon: '✕', label: 'Cancel', destructive: true },
// ]}
// onSelect={(opt) => handleOption(opt)}
// onClose={() => setShowSheet(false)}
// />
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
sheet: {
backgroundColor: '#fff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
paddingBottom: 34, // Safe area
},
handle: {
width: 40,
height: 4,
backgroundColor: '#e0e0e0',
borderRadius: 2,
alignSelf: 'center',
marginTop: 12,
marginBottom: 20,
},
option: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 24,
},
optionIcon: {
fontSize: 20,
marginRight: 16,
},
optionText: {
fontSize: 16,
color: '#1a1a1a',
},
destructiveText: {
color: '#f44336',
},
});
Pattern: Toast Notification
function Toast({ visible, message, type = 'info' }) {
if (!visible) return null;
const backgroundColor = {
info: '#323232',
success: '#4CAF50',
error: '#f44336',
}[type];
return (
<View style={[styles.toast, { backgroundColor }]}>
<Text style={styles.toastText}>{message}</Text>
</View>
);
}
const styles = StyleSheet.create({
toast: {
position: 'absolute',
bottom: 100,
left: 24,
right: 24,
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: 'center',
},
toastText: {
color: '#fff',
fontSize: 15,
fontWeight: '500',
},
});
Empty States & Loading
Empty states and loading indicators need clear visual communication. These patterns ensure users always know what's happening.
Pattern: Empty State
function EmptyState({ icon, title, message, actionLabel, onAction }) {
return (
<View style={styles.container}>
<Text style={styles.icon}>{icon}</Text>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>{message}</Text>
{actionLabel && (
<Pressable style={styles.actionButton} onPress={onAction}>
<Text style={styles.actionText}>{actionLabel}</Text>
</Pressable>
)}
</View>
);
}
// Usage:
// <EmptyState
// icon="📭"
// title="No Messages Yet"
// message="Start a conversation to see your messages here"
// actionLabel="New Message"
// onAction={() => navigation.navigate('Compose')}
// />
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 32,
},
icon: {
fontSize: 64,
marginBottom: 16,
},
title: {
fontSize: 20,
fontWeight: '600',
color: '#1a1a1a',
textAlign: 'center',
marginBottom: 8,
},
message: {
fontSize: 15,
color: '#666',
textAlign: 'center',
lineHeight: 22,
marginBottom: 24,
},
actionButton: {
backgroundColor: '#2196F3',
paddingVertical: 12,
paddingHorizontal: 32,
borderRadius: 24,
},
actionText: {
color: '#fff',
fontSize: 15,
fontWeight: '600',
},
});
Pattern: Loading Screen
function LoadingScreen({ message = 'Loading...' }) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#2196F3" />
<Text style={styles.message}>{message}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
message: {
marginTop: 16,
fontSize: 16,
color: '#666',
},
});
Pattern: Skeleton Loading
function SkeletonCard() {
return (
<View style={styles.card}>
<View style={styles.skeletonImage} />
<View style={styles.skeletonContent}>
<View style={[styles.skeletonLine, { width: '70%' }]} />
<View style={[styles.skeletonLine, { width: '90%' }]} />
<View style={[styles.skeletonLine, { width: '50%' }]} />
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
padding: 16,
backgroundColor: '#fff',
},
skeletonImage: {
width: 80,
height: 80,
backgroundColor: '#e0e0e0',
borderRadius: 8,
},
skeletonContent: {
flex: 1,
marginLeft: 12,
justifyContent: 'center',
gap: 8,
},
skeletonLine: {
height: 14,
backgroundColor: '#e0e0e0',
borderRadius: 4,
},
});
Composing Patterns Together
Real screens combine multiple patterns. Here's how to think about composition:
flowchart TB
subgraph Screen["Complete Screen"]
direction TB
H["Header Pattern"]
C["Content Area"]
F["Footer Pattern"]
end
subgraph ContentArea["Content Breakdown"]
Card1["Card Pattern"]
List["List Items"]
Grid["Grid Pattern"]
end
C --> ContentArea
style Screen fill:#e3f2fd
style ContentArea fill:#e8f5e9
Example: Complete Profile Screen
function ProfileScreen() {
return (
<SafeAreaView style={styles.safeArea}>
{/* Header */}
<HeaderWithActions
title="Profile"
onBack={() => navigation.goBack()}
onSave={() => handleSave()}
/>
<ScrollView style={styles.content}>
{/* Profile Card */}
<View style={styles.profileSection}>
<ContactListItem
name="Jane Developer"
subtitle="React Native Engineer"
avatarColor="#9c27b0"
isOnline={true}
/>
</View>
{/* Stats */}
<StatsCard stats={[
{ value: '142', label: 'Projects' },
{ value: '8.5k', label: 'Followers' },
{ value: '284', label: 'Following' },
]} />
{/* Settings Section */}
<FormSection title="Settings">
<SettingsToggleItem
icon="🔔"
title="Notifications"
subtitle="Receive push notifications"
value={notifications}
onValueChange={setNotifications}
/>
<IconListItem icon="🔒" title="Privacy" onPress={() => {}} />
<IconListItem icon="❓" title="Help" onPress={() => {}} />
</FormSection>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
},
profileSection: {
backgroundColor: '#fff',
marginBottom: 16,
},
});
✅ Composition Best Practices
- Start with scaffold: Choose your screen structure first
- Identify regions: Break the screen into header, content sections, footer
- Match patterns to data: Use cards for grouped info, lists for repeating items
- Keep patterns small: Each pattern should do one thing well
- Customize via props: Make patterns flexible with good prop interfaces
Hands-On Exercises
Exercise 1: Build a Settings Screen
Create a settings screen with a header, multiple sections (Account, Notifications, Privacy), and various list item types including toggles.
Show Solution Approach
Combine these patterns:
ScreenWithHeaderscaffoldFormSectionfor each settings groupIconListItemfor navigation itemsSettingsToggleItemfor toggles
function SettingsScreen() {
const [darkMode, setDarkMode] = useState(false);
const [notifications, setNotifications] = useState(true);
return (
<ScreenWithHeader title="Settings">
<FormSection title="Account">
<IconListItem icon="👤" title="Edit Profile" onPress={() => {}} />
<IconListItem icon="🔑" title="Change Password" onPress={() => {}} />
</FormSection>
<FormSection title="Preferences">
<SettingsToggleItem
icon="🌙"
title="Dark Mode"
value={darkMode}
onValueChange={setDarkMode}
/>
<SettingsToggleItem
icon="🔔"
title="Notifications"
value={notifications}
onValueChange={setNotifications}
/>
</FormSection>
<FormSection title="Support">
<IconListItem icon="❓" title="Help Center" onPress={() => {}} />
<IconListItem icon="📧" title="Contact Us" onPress={() => {}} />
</FormSection>
</ScreenWithHeader>
);
}
Exercise 2: Build a Product Grid
Create a 2-column product grid where each product shows an image, name, and price using the vertical card pattern.
Show Solution
function ProductGrid({ products }) {
return (
<View style={styles.grid}>
{products.map((product) => (
<Pressable key={product.id} style={styles.productCard}>
<Image source={{ uri: product.image }} style={styles.productImage} />
<View style={styles.productInfo}>
<Text style={styles.productName} numberOfLines={2}>
{product.name}
</Text>
<Text style={styles.productPrice}>${product.price}</Text>
</View>
</Pressable>
))}
</View>
);
}
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
padding: 12,
},
productCard: {
width: '47%',
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
productImage: {
width: '100%',
aspectRatio: 1,
backgroundColor: '#f5f5f5',
},
productInfo: {
padding: 12,
},
productName: {
fontSize: 14,
color: '#1a1a1a',
marginBottom: 4,
},
productPrice: {
fontSize: 16,
fontWeight: '700',
color: '#4CAF50',
},
});
Summary
You now have a comprehensive library of layout patterns to draw from. These patterns cover the vast majority of mobile UI needs.
🎯 Pattern Categories
- Screen Scaffolds: Basic, Header, Header+Footer, Tab Screen
- Headers: Simple, Back+Title, Actions, Search, Logo
- Cards: Content, Horizontal, Vertical, Stats
- List Items: Simple, Icon, Contact, Toggle
- Forms: Labeled Input, Sections, Complete Forms
- Grids: 2-column, 3-column Icons, Photo Gallery, Dashboard
- Overlays: Centered Modal, Bottom Sheet, Toast
- States: Empty, Loading, Skeleton
Using This Reference
Bookmark this lesson. When building new screens, identify which patterns you need, copy the relevant code, and customize. Over time, consider extracting these into your own component library for even faster development.
Coming Up Next
In the next lesson, we'll explore Responsive Design Without Media Queries. You'll learn how to make your layouts adapt to different screen sizes, orientations, and device types using React Native's tools and patterns.