Module Overview
Estimated Time: 4 hours | Difficulty: Beginner-Intermediate | Prerequisites: TypeScript basics
- View and SafeAreaView containers
- Text and typography
- Image optimization and caching
- TextInput and keyboard handling
- Pressable and touch interactions
- Platform-specific components
Component Mapping
React Native components compile to native platform components:| React Native | iOS (UIKit) | Android | Web Equivalent |
|---|---|---|---|
<View> | UIView | android.view.View | <div> |
<Text> | UILabel | TextView | <p>, <span> |
<Image> | UIImageView | ImageView | <img> |
<TextInput> | UITextField | EditText | <input> |
<ScrollView> | UIScrollView | ScrollView | <div> with overflow |
<FlatList> | UITableView | RecyclerView | Virtual list |
<Pressable> | UIButton | Button | <button> |
View Component
View is the most fundamental component—a container that supports layout, styling, touch handling, and accessibility.
Copy
import { View, StyleSheet } from 'react-native';
function ViewExample() {
return (
<View style={styles.container}>
<View style={styles.row}>
<View style={[styles.box, styles.red]} />
<View style={[styles.box, styles.green]} />
<View style={[styles.box, styles.blue]} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
box: {
width: 80,
height: 80,
borderRadius: 8,
},
red: { backgroundColor: '#ef4444' },
green: { backgroundColor: '#22c55e' },
blue: { backgroundColor: '#3b82f6' },
});
View Props
Copy
<View
// Styling
style={styles.container}
// Accessibility
accessible={true}
accessibilityLabel="Main container"
accessibilityRole="none"
accessibilityHint="Contains the main content"
// Touch handling
onTouchStart={(e) => console.log('Touch started')}
onTouchMove={(e) => console.log('Touch moved')}
onTouchEnd={(e) => console.log('Touch ended')}
// Pointer events
pointerEvents="auto" // 'auto' | 'none' | 'box-none' | 'box-only'
// Layout
onLayout={(e) => {
const { x, y, width, height } = e.nativeEvent.layout;
console.log('Layout:', { x, y, width, height });
}}
// Testing
testID="main-container"
>
{/* Children */}
</View>
SafeAreaView
Renders content within safe area boundaries (avoiding notches, home indicators):Copy
import { SafeAreaView, View, Text, StyleSheet } from 'react-native';
// Basic SafeAreaView (iOS only)
function BasicSafeArea() {
return (
<SafeAreaView style={styles.container}>
<Text>This avoids the notch!</Text>
</SafeAreaView>
);
}
// Better: react-native-safe-area-context (cross-platform)
import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
function App() {
return (
<SafeAreaProvider>
<MainScreen />
</SafeAreaProvider>
);
}
function MainScreen() {
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<Text>Safe on all platforms!</Text>
</View>
);
}
// Or use SafeAreaView component
function AlternativeScreen() {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<Text>Safe content</Text>
</SafeAreaView>
);
}
Text Component
All text must be wrapped in a<Text> component. Unlike web, you cannot put text directly in a <View>.
Copy
import { Text, StyleSheet, View } from 'react-native';
function TextExample() {
return (
<View style={styles.container}>
{/* Basic text */}
<Text style={styles.title}>Welcome to React Native</Text>
{/* Nested text (inherits parent styles) */}
<Text style={styles.paragraph}>
This is <Text style={styles.bold}>bold</Text> and this is{' '}
<Text style={styles.italic}>italic</Text> and this is{' '}
<Text style={styles.link} onPress={() => console.log('Link pressed')}>
a link
</Text>.
</Text>
{/* Truncated text */}
<Text numberOfLines={2} ellipsizeMode="tail" style={styles.truncated}>
This is a very long text that will be truncated after two lines.
You won't see the rest of this text because it exceeds the limit
that we have set for this particular text component.
</Text>
{/* Selectable text */}
<Text selectable style={styles.selectable}>
Long press to select this text
</Text>
{/* Adjustable font size */}
<Text
adjustsFontSizeToFit
numberOfLines={1}
style={styles.adjustable}
>
This text will shrink to fit on one line
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#1f2937',
marginBottom: 16,
},
paragraph: {
fontSize: 16,
lineHeight: 24,
color: '#4b5563',
marginBottom: 16,
},
bold: {
fontWeight: 'bold',
},
italic: {
fontStyle: 'italic',
},
link: {
color: '#3b82f6',
textDecorationLine: 'underline',
},
truncated: {
fontSize: 14,
color: '#6b7280',
marginBottom: 16,
},
selectable: {
fontSize: 14,
color: '#059669',
marginBottom: 16,
},
adjustable: {
fontSize: 24,
fontWeight: 'bold',
},
});
Text Props Reference
Copy
<Text
// Content control
numberOfLines={2}
ellipsizeMode="tail" // 'head' | 'middle' | 'tail' | 'clip'
// Selection
selectable={true}
selectionColor="#3b82f6"
// Accessibility
accessible={true}
accessibilityRole="header" // 'header' | 'link' | 'button' | etc.
accessibilityLabel="Welcome message"
// Press handling
onPress={() => console.log('Pressed')}
onLongPress={() => console.log('Long pressed')}
onPressIn={() => console.log('Press in')}
onPressOut={() => console.log('Press out')}
// Layout
onTextLayout={(e) => {
const { lines } = e.nativeEvent;
console.log('Number of lines:', lines.length);
}}
// iOS specific
adjustsFontSizeToFit={true}
minimumFontScale={0.5}
allowFontScaling={true}
// Android specific
textBreakStrategy="highQuality" // 'simple' | 'highQuality' | 'balanced'
android_hyphenationFrequency="normal"
>
Text content
</Text>
Image Component
Display images from local files, network URLs, or base64 data.Local Images
Copy
import { Image, StyleSheet, View } from 'react-native';
function LocalImageExample() {
return (
<View style={styles.container}>
{/* Local image (bundled with app) */}
<Image
source={require('../assets/images/logo.png')}
style={styles.logo}
/>
{/* With explicit dimensions */}
<Image
source={require('../assets/images/hero.png')}
style={{ width: 300, height: 200 }}
resizeMode="contain"
/>
</View>
);
}
Network Images
Copy
import { Image, StyleSheet, View, ActivityIndicator } from 'react-native';
import { useState } from 'react';
function NetworkImageExample() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
return (
<View style={styles.imageContainer}>
{loading && (
<ActivityIndicator
style={StyleSheet.absoluteFill}
size="large"
color="#3b82f6"
/>
)}
<Image
source={{
uri: 'https://picsum.photos/400/300',
// Optional headers for authenticated requests
headers: {
Authorization: 'Bearer token',
},
// Cache control (iOS)
cache: 'default', // 'default' | 'reload' | 'force-cache' | 'only-if-cached'
}}
style={styles.networkImage}
resizeMode="cover"
onLoadStart={() => setLoading(true)}
onLoadEnd={() => setLoading(false)}
onError={(e) => {
setError(true);
setLoading(false);
console.error('Image load error:', e.nativeEvent.error);
}}
// Fallback for errors
defaultSource={require('../assets/images/placeholder.png')}
// Blur placeholder (iOS)
blurRadius={loading ? 10 : 0}
/>
{error && (
<View style={styles.errorOverlay}>
<Text>Failed to load image</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
imageContainer: {
width: 300,
height: 200,
backgroundColor: '#f3f4f6',
borderRadius: 12,
overflow: 'hidden',
},
networkImage: {
width: '100%',
height: '100%',
},
errorOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
});
ImageBackground
Copy
import { ImageBackground, Text, StyleSheet, View } from 'react-native';
function ImageBackgroundExample() {
return (
<ImageBackground
source={require('../assets/images/background.jpg')}
style={styles.background}
resizeMode="cover"
imageStyle={styles.backgroundImage}
>
<View style={styles.overlay}>
<Text style={styles.title}>Welcome</Text>
<Text style={styles.subtitle}>Your journey starts here</Text>
</View>
</ImageBackground>
);
}
const styles = StyleSheet.create({
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
backgroundImage: {
opacity: 0.8,
},
overlay: {
backgroundColor: 'rgba(0,0,0,0.4)',
padding: 40,
borderRadius: 16,
alignItems: 'center',
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
subtitle: {
fontSize: 18,
color: '#e5e7eb',
},
});
Image Optimization with expo-image
For better performance, useexpo-image:
Copy
npx expo install expo-image
Copy
import { Image } from 'expo-image';
function OptimizedImage() {
return (
<Image
source="https://picsum.photos/400/300"
style={{ width: 300, height: 200 }}
contentFit="cover"
transition={200}
placeholder={require('../assets/images/placeholder.png')}
// Or use blurhash
placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
cachePolicy="memory-disk"
/>
);
}
TextInput Component
For user text input with full keyboard control.Copy
import { TextInput, View, Text, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
import { useState, useRef } from 'react';
function TextInputExample() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [bio, setBio] = useState('');
const emailRef = useRef<TextInput>(null);
const passwordRef = useRef<TextInput>(null);
const bioRef = useRef<TextInput>(null);
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
{/* Basic text input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Name</Text>
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder="Enter your name"
placeholderTextColor="#9ca3af"
autoCapitalize="words"
autoCorrect={false}
returnKeyType="next"
onSubmitEditing={() => emailRef.current?.focus()}
blurOnSubmit={false}
/>
</View>
{/* Email input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
ref={emailRef}
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="Enter your email"
placeholderTextColor="#9ca3af"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
textContentType="emailAddress"
returnKeyType="next"
onSubmitEditing={() => passwordRef.current?.focus()}
blurOnSubmit={false}
/>
</View>
{/* Password input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Password</Text>
<TextInput
ref={passwordRef}
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="Enter your password"
placeholderTextColor="#9ca3af"
secureTextEntry
autoComplete="password"
textContentType="password"
returnKeyType="next"
onSubmitEditing={() => bioRef.current?.focus()}
blurOnSubmit={false}
/>
</View>
{/* Multiline input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Bio</Text>
<TextInput
ref={bioRef}
style={[styles.input, styles.multiline]}
value={bio}
onChangeText={setBio}
placeholder="Tell us about yourself"
placeholderTextColor="#9ca3af"
multiline
numberOfLines={4}
textAlignVertical="top"
maxLength={500}
returnKeyType="done"
/>
<Text style={styles.charCount}>{bio.length}/500</Text>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
color: '#1f2937',
},
multiline: {
height: 120,
textAlignVertical: 'top',
},
charCount: {
fontSize: 12,
color: '#9ca3af',
textAlign: 'right',
marginTop: 4,
},
});
TextInput Props Reference
Copy
<TextInput
// Value
value={text}
onChangeText={(text) => setText(text)}
defaultValue="Initial value"
// Appearance
placeholder="Enter text..."
placeholderTextColor="#999"
style={styles.input}
// Keyboard
keyboardType="default" // 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'decimal-pad' | 'url'
keyboardAppearance="light" // iOS: 'default' | 'light' | 'dark'
returnKeyType="done" // 'done' | 'go' | 'next' | 'search' | 'send'
// Behavior
autoCapitalize="sentences" // 'none' | 'sentences' | 'words' | 'characters'
autoCorrect={true}
autoComplete="email" // 'email' | 'password' | 'username' | 'name' | etc.
textContentType="emailAddress" // iOS autofill
// Security
secureTextEntry={false}
// Multiline
multiline={false}
numberOfLines={4}
textAlignVertical="top" // Android: 'auto' | 'top' | 'bottom' | 'center'
// Limits
maxLength={100}
// Selection
selection={{ start: 0, end: 5 }}
selectionColor="#3b82f6"
// Events
onFocus={() => console.log('Focused')}
onBlur={() => console.log('Blurred')}
onSubmitEditing={() => console.log('Submitted')}
onEndEditing={() => console.log('Editing ended')}
onSelectionChange={(e) => console.log(e.nativeEvent.selection)}
// Refs
ref={inputRef}
blurOnSubmit={true}
// Accessibility
accessible={true}
accessibilityLabel="Email input"
/>
Pressable Component
Modern, flexible touch handling component (recommended over TouchableOpacity):Copy
import { Pressable, Text, StyleSheet, View } from 'react-native';
function PressableExample() {
return (
<View style={styles.container}>
{/* Basic pressable */}
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
onPress={() => console.log('Pressed')}
>
{({ pressed }) => (
<Text style={[styles.buttonText, pressed && styles.textPressed]}>
{pressed ? 'Pressing...' : 'Press Me'}
</Text>
)}
</Pressable>
{/* With all press events */}
<Pressable
style={styles.button}
onPress={() => console.log('onPress')}
onPressIn={() => console.log('onPressIn')}
onPressOut={() => console.log('onPressOut')}
onLongPress={() => console.log('onLongPress')}
delayLongPress={500}
hitSlop={10} // Increase touch area
pressRetentionOffset={{ top: 10, left: 10, right: 10, bottom: 10 }}
>
<Text style={styles.buttonText}>All Events</Text>
</Pressable>
{/* Android ripple effect */}
<Pressable
style={styles.button}
android_ripple={{
color: 'rgba(255, 255, 255, 0.3)',
borderless: false,
foreground: true,
}}
onPress={() => console.log('Ripple!')}
>
<Text style={styles.buttonText}>Android Ripple</Text>
</Pressable>
{/* Disabled state */}
<Pressable
style={({ pressed }) => [
styles.button,
styles.buttonDisabled,
]}
disabled={true}
onPress={() => console.log('Will not fire')}
>
<Text style={[styles.buttonText, styles.textDisabled]}>
Disabled
</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
gap: 16,
},
button: {
backgroundColor: '#3b82f6',
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
buttonPressed: {
backgroundColor: '#2563eb',
transform: [{ scale: 0.98 }],
},
buttonDisabled: {
backgroundColor: '#9ca3af',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
textPressed: {
opacity: 0.8,
},
textDisabled: {
opacity: 0.6,
},
});
Custom Button Component
Copy
import { Pressable, Text, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
style?: ViewStyle;
textStyle?: TextStyle;
}
export function Button({
title,
onPress,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
icon,
style,
textStyle,
}: ButtonProps) {
const isDisabled = disabled || loading;
return (
<Pressable
style={({ pressed }) => [
styles.base,
styles[variant],
styles[size],
pressed && !isDisabled && styles.pressed,
isDisabled && styles.disabled,
style,
]}
onPress={onPress}
disabled={isDisabled}
>
{loading ? (
<ActivityIndicator
color={variant === 'outline' || variant === 'ghost' ? '#3b82f6' : '#fff'}
size="small"
/>
) : (
<>
{icon}
<Text
style={[
styles.text,
styles[`${variant}Text`],
styles[`${size}Text`],
isDisabled && styles.disabledText,
textStyle,
]}
>
{title}
</Text>
</>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
gap: 8,
},
// Variants
primary: {
backgroundColor: '#3b82f6',
},
secondary: {
backgroundColor: '#6b7280',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#3b82f6',
},
ghost: {
backgroundColor: 'transparent',
},
// Sizes
sm: {
paddingVertical: 8,
paddingHorizontal: 16,
},
md: {
paddingVertical: 12,
paddingHorizontal: 20,
},
lg: {
paddingVertical: 16,
paddingHorizontal: 24,
},
// States
pressed: {
opacity: 0.8,
transform: [{ scale: 0.98 }],
},
disabled: {
opacity: 0.5,
},
// Text
text: {
fontWeight: '600',
},
primaryText: {
color: '#fff',
},
secondaryText: {
color: '#fff',
},
outlineText: {
color: '#3b82f6',
},
ghostText: {
color: '#3b82f6',
},
smText: {
fontSize: 14,
},
mdText: {
fontSize: 16,
},
lgText: {
fontSize: 18,
},
disabledText: {
opacity: 0.7,
},
});
ScrollView Component
For scrollable content:Copy
import { ScrollView, View, Text, StyleSheet, RefreshControl } from 'react-native';
import { useState, useCallback } from 'react';
function ScrollViewExample() {
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
// Simulate API call
setTimeout(() => setRefreshing(false), 2000);
}, []);
return (
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#3b82f6"
colors={['#3b82f6']} // Android
/>
}
// Scroll behavior
bounces={true} // iOS bounce effect
overScrollMode="always" // Android: 'auto' | 'always' | 'never'
scrollEventThrottle={16} // For smooth scroll tracking
onScroll={(e) => {
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
const isCloseToBottom =
layoutMeasurement.height + contentOffset.y >= contentSize.height - 20;
if (isCloseToBottom) {
console.log('Near bottom!');
}
}}
// Keyboard
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
>
{Array.from({ length: 20 }).map((_, i) => (
<View key={i} style={styles.item}>
<Text style={styles.itemText}>Item {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
// Horizontal ScrollView
function HorizontalScrollExample() {
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalContent}
pagingEnabled={false} // Set true for paging behavior
snapToInterval={160} // Snap to card width
decelerationRate="fast"
>
{Array.from({ length: 10 }).map((_, i) => (
<View key={i} style={styles.card}>
<Text style={styles.cardText}>Card {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
contentContainer: {
padding: 20,
},
item: {
backgroundColor: '#fff',
padding: 20,
marginBottom: 12,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
itemText: {
fontSize: 16,
color: '#1f2937',
},
horizontalContent: {
paddingHorizontal: 20,
paddingVertical: 10,
},
card: {
width: 150,
height: 100,
backgroundColor: '#3b82f6',
borderRadius: 12,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
cardText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
Platform-Specific Code
Copy
import { Platform, StyleSheet, View, Text } from 'react-native';
function PlatformExample() {
return (
<View style={styles.container}>
<Text style={styles.text}>
Running on {Platform.OS} {Platform.Version}
</Text>
{/* Platform-specific rendering */}
{Platform.OS === 'ios' && <Text>iOS only content</Text>}
{Platform.OS === 'android' && <Text>Android only content</Text>}
{/* Platform.select */}
<Text style={styles.platformText}>
{Platform.select({
ios: 'Hello iOS!',
android: 'Hello Android!',
default: 'Hello!',
})}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
text: {
fontSize: 16,
marginBottom: 10,
},
platformText: {
fontSize: 18,
fontWeight: 'bold',
...Platform.select({
ios: {
color: '#007AFF',
},
android: {
color: '#3DDC84',
},
}),
},
});
// Platform-specific files
// Button.ios.tsx - iOS implementation
// Button.android.tsx - Android implementation
// Button.tsx - Default/shared implementation
Hands-On Exercise: Profile Card
Build a complete profile card component:Copy
import { View, Text, Image, Pressable, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
interface ProfileCardProps {
name: string;
title: string;
avatar: string;
followers: number;
following: number;
posts: number;
isFollowing: boolean;
onFollowPress: () => void;
onMessagePress: () => void;
}
export function ProfileCard({
name,
title,
avatar,
followers,
following,
posts,
isFollowing,
onFollowPress,
onMessagePress,
}: ProfileCardProps) {
return (
<View style={styles.card}>
<Image source={{ uri: avatar }} style={styles.avatar} />
<Text style={styles.name}>{name}</Text>
<Text style={styles.title}>{title}</Text>
<View style={styles.stats}>
<View style={styles.stat}>
<Text style={styles.statValue}>{posts}</Text>
<Text style={styles.statLabel}>Posts</Text>
</View>
<View style={styles.stat}>
<Text style={styles.statValue}>{followers}</Text>
<Text style={styles.statLabel}>Followers</Text>
</View>
<View style={styles.stat}>
<Text style={styles.statValue}>{following}</Text>
<Text style={styles.statLabel}>Following</Text>
</View>
</View>
<View style={styles.actions}>
<Pressable
style={({ pressed }) => [
styles.button,
isFollowing ? styles.followingButton : styles.followButton,
pressed && styles.buttonPressed,
]}
onPress={onFollowPress}
>
<Text style={[
styles.buttonText,
isFollowing && styles.followingButtonText,
]}>
{isFollowing ? 'Following' : 'Follow'}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.button,
styles.messageButton,
pressed && styles.buttonPressed,
]}
onPress={onMessagePress}
>
<Ionicons name="chatbubble-outline" size={18} color="#3b82f6" />
<Text style={styles.messageButtonText}>Message</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
marginBottom: 16,
},
name: {
fontSize: 22,
fontWeight: 'bold',
color: '#1f2937',
marginBottom: 4,
},
title: {
fontSize: 14,
color: '#6b7280',
marginBottom: 20,
},
stats: {
flexDirection: 'row',
marginBottom: 24,
},
stat: {
alignItems: 'center',
paddingHorizontal: 24,
},
statValue: {
fontSize: 20,
fontWeight: 'bold',
color: '#1f2937',
},
statLabel: {
fontSize: 12,
color: '#9ca3af',
marginTop: 4,
},
actions: {
flexDirection: 'row',
gap: 12,
},
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
gap: 6,
},
buttonPressed: {
opacity: 0.8,
transform: [{ scale: 0.98 }],
},
followButton: {
backgroundColor: '#3b82f6',
},
followingButton: {
backgroundColor: '#f3f4f6',
borderWidth: 1,
borderColor: '#e5e7eb',
},
messageButton: {
backgroundColor: '#eff6ff',
borderWidth: 1,
borderColor: '#bfdbfe',
},
buttonText: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
},
followingButtonText: {
color: '#374151',
},
messageButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#3b82f6',
},
});
Next Steps
Module 6: Styling & Theming
Learn advanced styling patterns, theming, and dark mode implementation