Skip to main content
Core Components

Module Overview

Estimated Time: 4 hours | Difficulty: Beginner-Intermediate | Prerequisites: TypeScript basics
React Native provides a set of core components that map directly to native platform views. Unlike web development where you use HTML elements, React Native uses components that render to actual native UI elements. What You’ll Learn:
  • 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 NativeiOS (UIKit)AndroidWeb Equivalent
<View>UIViewandroid.view.View<div>
<Text>UILabelTextView<p>, <span>
<Image>UIImageViewImageView<img>
<TextInput>UITextFieldEditText<input>
<ScrollView>UIScrollViewScrollView<div> with overflow
<FlatList>UITableViewRecyclerViewVirtual list
<Pressable>UIButtonButton<button>

View Component

View is the most fundamental component—a container that supports layout, styling, touch handling, and accessibility.
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

<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):
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>.
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

<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

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

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

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, use expo-image:
npx expo install expo-image
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.
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

<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):
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

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:
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

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:
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