Skip to main content

🔘 Switch, ActivityIndicator, StatusBar

Essential utility components for toggles, loading states, and system UI control

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Create boolean toggles with the Switch component
  • Customize Switch appearance for both platforms
  • Display loading indicators with ActivityIndicator
  • Control the device status bar appearance
  • Handle status bar in different screen contexts
  • Combine these components in real-world scenarios

⏱️ Estimated Time: 20-25 minutes

📑 In This Lesson

Switch — Boolean Toggles

The Switch component renders a platform-native toggle switch. It's perfect for on/off settings, feature flags, or any boolean choice.

📖 What is Switch?

Switch is a controlled component that displays a toggle. Users can tap it to switch between on (true) and off (false) states. It renders using native platform controls, so it looks and feels familiar to users.

Switch Appearance by Platform iOS Off On Android Off On Switch uses native platform controls for familiar UX

Switch renders differently on iOS and Android, using each platform's native toggle style

Basic Switch Usage

import { useState } from 'react';
import { Switch, View, Text, StyleSheet } from 'react-native';

function SettingsToggle() {
  const [isEnabled, setIsEnabled] = useState(false);

  return (
    <View style={styles.row}>
      <Text style={styles.label}>Enable Notifications</Text>
      <Switch
        value={isEnabled}
        onValueChange={setIsEnabled}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 12,
    paddingHorizontal: 16,
  },
  label: {
    fontSize: 16,
    color: '#333',
  },
});

Switch Props

Prop Type Description
value boolean Current on/off state (required for controlled)
onValueChange (value: boolean) => void Called when user toggles
disabled boolean Prevents user interaction
trackColor { false?: string, true?: string } Background colors for off/on
thumbColor string Color of the circular thumb
ios_backgroundColor string iOS-only background color when off

Customizing Switch Colors

import { useState } from 'react';
import { Switch, View, Text, StyleSheet, Platform } from 'react-native';

function CustomSwitch() {
  const [isEnabled, setIsEnabled] = useState(false);

  return (
    <View style={styles.row}>
      <Text style={styles.label}>Dark Mode</Text>
      <Switch
        value={isEnabled}
        onValueChange={setIsEnabled}
        trackColor={{ 
          false: '#767577',  // Track color when OFF
          true: '#81b0ff'    // Track color when ON
        }}
        thumbColor={isEnabled ? '#2196F3' : '#f4f3f4'}  // Thumb color
        ios_backgroundColor="#3e3e3e"  // iOS background when OFF
      />
    </View>
  );
}

Settings List Pattern

import { useState } from 'react';
import { Switch, View, Text, StyleSheet } from 'react-native';

interface Setting {
  id: string;
  label: string;
  description?: string;
}

const SETTINGS: Setting[] = [
  { id: 'notifications', label: 'Push Notifications', description: 'Receive alerts for new messages' },
  { id: 'sounds', label: 'Sound Effects', description: 'Play sounds for actions' },
  { id: 'darkMode', label: 'Dark Mode', description: 'Use dark color scheme' },
  { id: 'analytics', label: 'Usage Analytics', description: 'Help improve the app' },
];

function SettingsList() {
  const [settings, setSettings] = useState<Record<string, boolean>>({
    notifications: true,
    sounds: true,
    darkMode: false,
    analytics: false,
  });

  const toggleSetting = (id: string) => {
    setSettings(prev => ({
      ...prev,
      [id]: !prev[id],
    }));
  };

  return (
    <View style={styles.container}>
      {SETTINGS.map((setting, index) => (
        <View 
          key={setting.id}
          style={[
            styles.row,
            index < SETTINGS.length - 1 && styles.rowBorder,
          ]}
        >
          <View style={styles.textContainer}>
            <Text style={styles.label}>{setting.label}</Text>
            {setting.description && (
              <Text style={styles.description}>{setting.description}</Text>
            )}
          </View>
          <Switch
            value={settings[setting.id]}
            onValueChange={() => toggleSetting(setting.id)}
            trackColor={{ false: '#ddd', true: '#81b0ff' }}
            thumbColor={settings[setting.id] ? '#2196F3' : '#f4f3f4'}
          />
        </View>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    borderRadius: 12,
    marginHorizontal: 16,
    overflow: 'hidden',
  },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
  },
  rowBorder: {
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  textContainer: {
    flex: 1,
    marginRight: 12,
  },
  label: {
    fontSize: 16,
    color: '#333',
    fontWeight: '500',
  },
  description: {
    fontSize: 13,
    color: '#666',
    marginTop: 2,
  },
});

✅ Accessibility Tip

Switch automatically handles accessibility, but you can improve it with:

<Switch
  accessibilityLabel="Enable notifications"
  accessibilityHint="Double tap to toggle"
/>

ActivityIndicator — Loading States

Users need to know when something is loading. The ActivityIndicator component shows a spinning indicator that uses native platform styling.

Basic Usage

import { ActivityIndicator, View, StyleSheet } from 'react-native';

function LoadingScreen() {
  return (
    <View style={styles.container}>
      <ActivityIndicator />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

ActivityIndicator Props

Prop Type Description
size 'small' | 'large' | number Size of indicator (number is Android only)
color string Color of the spinner
animating boolean Whether to show (default: true)
hidesWhenStopped boolean iOS: hide when not animating

Size and Color Variations

import { ActivityIndicator, View, StyleSheet } from 'react-native';

function LoadingVariations() {
  return (
    <View style={styles.container}>
      {/* Default (small, gray) */}
      <ActivityIndicator />
      
      {/* Large size */}
      <ActivityIndicator size="large" />
      
      {/* Custom color */}
      <ActivityIndicator size="large" color="#2196F3" />
      
      {/* Brand color */}
      <ActivityIndicator size="large" color="#667eea" />
      
      {/* On dark background */}
      <View style={styles.dark}>
        <ActivityIndicator size="large" color="white" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'space-around',
    alignItems: 'center',
    padding: 20,
  },
  dark: {
    backgroundColor: '#333',
    padding: 20,
    borderRadius: 10,
  },
});
small 20px large 36px size={50} Android only

ActivityIndicator sizes: small (20px), large (36px), or custom on Android

Conditional Loading Pattern

import { useState, useEffect } from 'react';
import { ActivityIndicator, View, Text, FlatList, StyleSheet } from 'react-native';

interface Item {
  id: string;
  title: string;
}

function DataList() {
  const [data, setData] = useState<Item[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    try {
      setLoading(true);
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      setData([
        { id: '1', title: 'Item 1' },
        { id: '2', title: 'Item 2' },
        { id: '3', title: 'Item 3' },
      ]);
    } catch (e) {
      setError('Failed to load data');
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#2196F3" />
        <Text style={styles.loadingText}>Loading...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.centered}>
        <Text style={styles.errorText}>{error}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={data}
      keyExtractor={item => item.id}
      renderItem={({ item }) => (
        <View style={styles.item}>
          <Text>{item.title}</Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  loadingText: {
    marginTop: 12,
    fontSize: 16,
    color: '#666',
  },
  errorText: {
    fontSize: 16,
    color: '#f44336',
  },
  item: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
});

Inline Loading Button

import { useState } from 'react';
import { ActivityIndicator, Pressable, Text, StyleSheet } from 'react-native';

function LoadingButton() {
  const [loading, setLoading] = useState(false);

  const handlePress = async () => {
    setLoading(true);
    await new Promise(resolve => setTimeout(resolve, 2000));
    setLoading(false);
  };

  return (
    <Pressable
      style={[styles.button, loading && styles.buttonDisabled]}
      onPress={handlePress}
      disabled={loading}
    >
      {loading ? (
        <ActivityIndicator color="white" />
      ) : (
        <Text style={styles.buttonText}>Submit</Text>
      )}
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#2196F3',
    paddingVertical: 14,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: 120,
    minHeight: 48,
  },
  buttonDisabled: {
    opacity: 0.7,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

Overlay Loading

import { ActivityIndicator, View, StyleSheet, Modal } from 'react-native';

interface LoadingOverlayProps {
  visible: boolean;
}

function LoadingOverlay({ visible }: LoadingOverlayProps) {
  return (
    <Modal transparent visible={visible}>
      <View style={styles.overlay}>
        <View style={styles.loaderContainer}>
          <ActivityIndicator size="large" color="#2196F3" />
        </View>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  loaderContainer: {
    backgroundColor: 'white',
    padding: 30,
    borderRadius: 12,
  },
});

StatusBar — System UI Control

The status bar sits at the top of the device screen showing time, battery, signal, etc. The StatusBar component lets you control its appearance.

Status Bar Styles 9:41 📶 🔋 Dark Background barStyle="light-content" 9:41 📶 🔋 Light Background barStyle="dark-content"

Match status bar style to your background: light-content on dark, dark-content on light

Basic Usage

import { StatusBar, View, StyleSheet } from 'react-native';

function App() {
  return (
    <View style={styles.container}>
      <StatusBar barStyle="dark-content" />
      {/* Your app content */}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
});

StatusBar Props

Prop Type Platform Description
barStyle 'default' | 'light-content' | 'dark-content' Both Color of text/icons
backgroundColor string Android Background color
translucent boolean Android Draw app under status bar
hidden boolean Both Hide the status bar
animated boolean Both Animate changes
networkActivityIndicatorVisible boolean iOS Show network spinner

barStyle Options

// Light text/icons (for dark backgrounds)
<StatusBar barStyle="light-content" />

// Dark text/icons (for light backgrounds)
<StatusBar barStyle="dark-content" />

// Platform default
<StatusBar barStyle="default" />
// iOS: dark-content
// Android: light-content

Android-Specific Styling

import { StatusBar, View, StyleSheet, Platform } from 'react-native';

function AndroidStatusBarExample() {
  return (
    <View style={styles.container}>
      <StatusBar
        barStyle="light-content"
        backgroundColor="#1976D2"  // Android only
        translucent={false}         // Android only
      />
      {/* Content */}
    </View>
  );
}

Hiding the Status Bar

import { useState } from 'react';
import { StatusBar, View, Pressable, Text, StyleSheet } from 'react-native';

function FullScreenMode() {
  const [hidden, setHidden] = useState(false);

  return (
    <View style={styles.container}>
      <StatusBar hidden={hidden} animated />
      
      <Pressable 
        style={styles.button}
        onPress={() => setHidden(!hidden)}
      >
        <Text style={styles.buttonText}>
          {hidden ? 'Show' : 'Hide'} Status Bar
        </Text>
      </Pressable>
    </View>
  );
}

Screen-Specific Status Bars

Different screens often need different status bar styles. You can include multiple StatusBar components:

// In a dark-themed screen
function ProfileScreen() {
  return (
    <View style={{ flex: 1, backgroundColor: '#1a1a2e' }}>
      <StatusBar barStyle="light-content" />
      {/* Dark screen content */}
    </View>
  );
}

// In a light-themed screen
function SettingsScreen() {
  return (
    <View style={{ flex: 1, backgroundColor: '#fff' }}>
      <StatusBar barStyle="dark-content" />
      {/* Light screen content */}
    </View>
  );
}

⚠️ StatusBar in Navigation

When using React Navigation or Expo Router, consider using useFocusEffect to update the status bar when screens focus:

import { useFocusEffect } from '@react-navigation/native';
import { StatusBar } from 'react-native';
import { useCallback } from 'react';

function DarkScreen() {
  useFocusEffect(
    useCallback(() => {
      StatusBar.setBarStyle('light-content');
    }, [])
  );
  // ...
}

Imperative API

StatusBar also has static methods for imperative updates:

import { StatusBar } from 'react-native';

// Change bar style
StatusBar.setBarStyle('light-content', true); // true = animated

// Hide/show
StatusBar.setHidden(true, 'fade'); // 'fade' or 'slide'

// Android background
StatusBar.setBackgroundColor('#2196F3', true);

// Android translucent
StatusBar.setTranslucent(true);

Combining Components

Let's put these components together in realistic scenarios.

Settings Screen

import { useState } from 'react';
import { 
  View, 
  Text, 
  Switch, 
  StatusBar, 
  ScrollView, 
  StyleSheet,
  Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

function SettingsScreen() {
  const [notifications, setNotifications] = useState(true);
  const [sounds, setSounds] = useState(true);
  const [darkMode, setDarkMode] = useState(false);
  const [autoPlay, setAutoPlay] = useState(false);

  const backgroundColor = darkMode ? '#1a1a2e' : '#f5f5f5';
  const cardBackground = darkMode ? '#2d2d44' : '#fff';
  const textColor = darkMode ? '#fff' : '#333';
  const subtitleColor = darkMode ? '#aaa' : '#666';

  return (
    <SafeAreaView style={[styles.container, { backgroundColor }]}>
      <StatusBar 
        barStyle={darkMode ? 'light-content' : 'dark-content'} 
        animated
      />
      
      <ScrollView>
        <Text style={[styles.header, { color: textColor }]}>Settings</Text>
        
        <View style={[styles.section, { backgroundColor: cardBackground }]}>
          <Text style={[styles.sectionTitle, { color: subtitleColor }]}>
            NOTIFICATIONS
          </Text>
          
          <View style={styles.row}>
            <View style={styles.rowText}>
              <Text style={[styles.rowLabel, { color: textColor }]}>
                Push Notifications
              </Text>
              <Text style={[styles.rowSubtitle, { color: subtitleColor }]}>
                Receive alerts for new messages
              </Text>
            </View>
            <Switch
              value={notifications}
              onValueChange={setNotifications}
              trackColor={{ false: '#767577', true: '#81b0ff' }}
              thumbColor={notifications ? '#2196F3' : '#f4f3f4'}
            />
          </View>
          
          <View style={[styles.row, styles.rowBorder]}>
            <View style={styles.rowText}>
              <Text style={[styles.rowLabel, { color: textColor }]}>
                Sound Effects
              </Text>
              <Text style={[styles.rowSubtitle, { color: subtitleColor }]}>
                Play sounds for actions
              </Text>
            </View>
            <Switch
              value={sounds}
              onValueChange={setSounds}
              trackColor={{ false: '#767577', true: '#81b0ff' }}
              thumbColor={sounds ? '#2196F3' : '#f4f3f4'}
            />
          </View>
        </View>

        <View style={[styles.section, { backgroundColor: cardBackground }]}>
          <Text style={[styles.sectionTitle, { color: subtitleColor }]}>
            APPEARANCE
          </Text>
          
          <View style={styles.row}>
            <View style={styles.rowText}>
              <Text style={[styles.rowLabel, { color: textColor }]}>
                Dark Mode
              </Text>
              <Text style={[styles.rowSubtitle, { color: subtitleColor }]}>
                Use dark color scheme
              </Text>
            </View>
            <Switch
              value={darkMode}
              onValueChange={setDarkMode}
              trackColor={{ false: '#767577', true: '#81b0ff' }}
              thumbColor={darkMode ? '#2196F3' : '#f4f3f4'}
            />
          </View>
        </View>

        <View style={[styles.section, { backgroundColor: cardBackground }]}>
          <Text style={[styles.sectionTitle, { color: subtitleColor }]}>
            MEDIA
          </Text>
          
          <View style={styles.row}>
            <View style={styles.rowText}>
              <Text style={[styles.rowLabel, { color: textColor }]}>
                Auto-Play Videos
              </Text>
              <Text style={[styles.rowSubtitle, { color: subtitleColor }]}>
                Play videos automatically in feed
              </Text>
            </View>
            <Switch
              value={autoPlay}
              onValueChange={setAutoPlay}
              trackColor={{ false: '#767577', true: '#81b0ff' }}
              thumbColor={autoPlay ? '#2196F3' : '#f4f3f4'}
            />
          </View>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    fontSize: 34,
    fontWeight: 'bold',
    padding: 20,
    paddingBottom: 10,
  },
  section: {
    marginHorizontal: 16,
    marginBottom: 20,
    borderRadius: 12,
    overflow: 'hidden',
  },
  sectionTitle: {
    fontSize: 13,
    fontWeight: '600',
    paddingHorizontal: 16,
    paddingTop: 16,
    paddingBottom: 8,
  },
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 16,
  },
  rowBorder: {
    borderTopWidth: 1,
    borderTopColor: 'rgba(0,0,0,0.05)',
  },
  rowText: {
    flex: 1,
    marginRight: 12,
  },
  rowLabel: {
    fontSize: 16,
    fontWeight: '500',
  },
  rowSubtitle: {
    fontSize: 13,
    marginTop: 2,
  },
});

Loading State with Status

import { useState, useEffect } from 'react';
import { 
  View, 
  Text, 
  ActivityIndicator, 
  StatusBar,
  StyleSheet,
} from 'react-native';

type Status = 'idle' | 'loading' | 'success' | 'error';

function DataFetcher() {
  const [status, setStatus] = useState<Status>('idle');
  const [data, setData] = useState<string | null>(null);

  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    setStatus('loading');
    try {
      await new Promise(resolve => setTimeout(resolve, 2000));
      setData('Data loaded successfully!');
      setStatus('success');
    } catch {
      setStatus('error');
    }
  };

  return (
    <View style={styles.container}>
      <StatusBar barStyle="dark-content" />
      
      {status === 'loading' && (
        <View style={styles.centered}>
          <ActivityIndicator size="large" color="#2196F3" />
          <Text style={styles.loadingText}>Fetching data...</Text>
        </View>
      )}
      
      {status === 'success' && (
        <View style={styles.centered}>
          <Text style={styles.successIcon}>✅</Text>
          <Text style={styles.successText}>{data}</Text>
        </View>
      )}
      
      {status === 'error' && (
        <View style={styles.centered}>
          <Text style={styles.errorIcon}>❌</Text>
          <Text style={styles.errorText}>Something went wrong</Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  loadingText: {
    marginTop: 16,
    fontSize: 16,
    color: '#666',
  },
  successIcon: {
    fontSize: 48,
    marginBottom: 16,
  },
  successText: {
    fontSize: 18,
    color: '#4CAF50',
    textAlign: 'center',
  },
  errorIcon: {
    fontSize: 48,
    marginBottom: 16,
  },
  errorText: {
    fontSize: 18,
    color: '#f44336',
    textAlign: 'center',
  },
});

Hands-On Exercises

Exercise 1: Toggle Settings Card

Goal: Create a settings card with multiple toggles.

Requirements:

  • Card with rounded corners and shadow
  • 3 settings: WiFi, Bluetooth, Airplane Mode
  • Custom colors for the switches (blue when on)
  • Airplane Mode disables WiFi and Bluetooth when enabled
💡 Hint

Use an effect or the onValueChange handler for Airplane Mode to disable the other settings when it's turned on.

✅ Solution
import { useState, useEffect } from 'react';
import { View, Text, Switch, StyleSheet } from 'react-native';

export default function SettingsCard() {
  const [wifi, setWifi] = useState(true);
  const [bluetooth, setBluetooth] = useState(false);
  const [airplane, setAirplane] = useState(false);

  // When airplane mode is enabled, disable others
  useEffect(() => {
    if (airplane) {
      setWifi(false);
      setBluetooth(false);
    }
  }, [airplane]);

  return (
    <View style={styles.card}>
      <View style={styles.row}>
        <Text style={styles.icon}>📶</Text>
        <Text style={[styles.label, airplane && styles.disabled]}>WiFi</Text>
        <Switch
          value={wifi}
          onValueChange={setWifi}
          disabled={airplane}
          trackColor={{ false: '#ddd', true: '#81b0ff' }}
          thumbColor={wifi ? '#2196F3' : '#f4f3f4'}
        />
      </View>
      
      <View style={[styles.row, styles.rowBorder]}>
        <Text style={styles.icon}>📡</Text>
        <Text style={[styles.label, airplane && styles.disabled]}>Bluetooth</Text>
        <Switch
          value={bluetooth}
          onValueChange={setBluetooth}
          disabled={airplane}
          trackColor={{ false: '#ddd', true: '#81b0ff' }}
          thumbColor={bluetooth ? '#2196F3' : '#f4f3f4'}
        />
      </View>
      
      <View style={[styles.row, styles.rowBorder]}>
        <Text style={styles.icon}>✈️</Text>
        <Text style={styles.label}>Airplane Mode</Text>
        <Switch
          value={airplane}
          onValueChange={setAirplane}
          trackColor={{ false: '#ddd', true: '#ffcc80' }}
          thumbColor={airplane ? '#FF9800' : '#f4f3f4'}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    borderRadius: 16,
    margin: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
  },
  rowBorder: {
    borderTopWidth: 1,
    borderTopColor: '#f0f0f0',
  },
  icon: {
    fontSize: 24,
    marginRight: 12,
  },
  label: {
    flex: 1,
    fontSize: 16,
    color: '#333',
  },
  disabled: {
    color: '#999',
  },
});

Exercise 2: Loading States Demo

Goal: Create a component showing different loading indicator styles.

Requirements:

  • Show small and large ActivityIndicators
  • One on light background, one on dark
  • A button that toggles loading state
  • Display "Loading..." text when active
✅ Solution
import { useState } from 'react';
import { View, Text, ActivityIndicator, Pressable, StyleSheet } from 'react-native';

export default function LoadingDemo() {
  const [loading, setLoading] = useState(true);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>ActivityIndicator Demo</Text>
      
      <View style={styles.row}>
        <View style={styles.lightBox}>
          <ActivityIndicator 
            animating={loading} 
            size="small" 
            color="#2196F3" 
          />
          <Text style={styles.lightText}>Small</Text>
        </View>
        
        <View style={styles.lightBox}>
          <ActivityIndicator 
            animating={loading} 
            size="large" 
            color="#2196F3" 
          />
          <Text style={styles.lightText}>Large</Text>
        </View>
      </View>
      
      <View style={styles.row}>
        <View style={styles.darkBox}>
          <ActivityIndicator 
            animating={loading} 
            size="small" 
            color="white" 
          />
          <Text style={styles.darkText}>On Dark</Text>
        </View>
        
        <View style={styles.darkBox}>
          <ActivityIndicator 
            animating={loading} 
            size="large" 
            color="#81b0ff" 
          />
          <Text style={styles.darkText}>Custom Color</Text>
        </View>
      </View>
      
      {loading && (
        <Text style={styles.loadingText}>Loading...</Text>
      )}
      
      <Pressable 
        style={styles.button}
        onPress={() => setLoading(!loading)}
      >
        <Text style={styles.buttonText}>
          {loading ? 'Stop' : 'Start'} Loading
        </Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 24,
    textAlign: 'center',
  },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 20,
  },
  lightBox: {
    backgroundColor: 'white',
    padding: 24,
    borderRadius: 12,
    alignItems: 'center',
    width: 140,
    shadowColor: '#000',
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
  },
  darkBox: {
    backgroundColor: '#333',
    padding: 24,
    borderRadius: 12,
    alignItems: 'center',
    width: 140,
  },
  lightText: {
    marginTop: 12,
    color: '#666',
  },
  darkText: {
    marginTop: 12,
    color: '#aaa',
  },
  loadingText: {
    textAlign: 'center',
    fontSize: 16,
    color: '#2196F3',
    marginBottom: 20,
  },
  button: {
    backgroundColor: '#2196F3',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

Exercise 3: Theme Toggle with StatusBar

Goal: Create a screen that toggles between light and dark themes.

Requirements:

  • Switch to toggle dark mode
  • StatusBar updates to match theme (light-content/dark-content)
  • Background and text colors change
  • Smooth animated transition
✅ Solution
import { useState } from 'react';
import { View, Text, Switch, StatusBar, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

export default function ThemeToggle() {
  const [darkMode, setDarkMode] = useState(false);

  const theme = {
    background: darkMode ? '#1a1a2e' : '#fff',
    card: darkMode ? '#2d2d44' : '#f5f5f5',
    text: darkMode ? '#fff' : '#333',
    subtitle: darkMode ? '#aaa' : '#666',
  };

  return (
    <SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
      <StatusBar 
        barStyle={darkMode ? 'light-content' : 'dark-content'}
        animated
      />
      
      <Text style={[styles.title, { color: theme.text }]}>
        Theme Settings
      </Text>
      
      <View style={[styles.card, { backgroundColor: theme.card }]}>
        <View style={styles.row}>
          <View>
            <Text style={[styles.label, { color: theme.text }]}>
              Dark Mode
            </Text>
            <Text style={[styles.subtitle, { color: theme.subtitle }]}>
              {darkMode ? 'Currently using dark theme' : 'Currently using light theme'}
            </Text>
          </View>
          <Switch
            value={darkMode}
            onValueChange={setDarkMode}
            trackColor={{ false: '#ddd', true: '#81b0ff' }}
            thumbColor={darkMode ? '#2196F3' : '#f4f3f4'}
          />
        </View>
      </View>
      
      <View style={[styles.preview, { backgroundColor: theme.card }]}>
        <Text style={[styles.previewTitle, { color: theme.text }]}>
          Preview
        </Text>
        <Text style={[styles.previewText, { color: theme.subtitle }]}>
          This is how content looks in {darkMode ? 'dark' : 'light'} mode.
          The status bar icons are now {darkMode ? 'light' : 'dark'}.
        </Text>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    padding: 20,
  },
  card: {
    marginHorizontal: 16,
    borderRadius: 12,
    padding: 16,
  },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  label: {
    fontSize: 18,
    fontWeight: '600',
  },
  subtitle: {
    fontSize: 14,
    marginTop: 4,
  },
  preview: {
    margin: 16,
    padding: 20,
    borderRadius: 12,
  },
  previewTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 8,
  },
  previewText: {
    fontSize: 14,
    lineHeight: 22,
  },
});

Summary

🎉 Key Takeaways

  • Switch is for boolean toggles with value and onValueChange
  • Customize Switch with trackColor, thumbColor, and ios_backgroundColor
  • ActivityIndicator shows native loading spinners
  • Use size="large" for prominent loading states, color to match your theme
  • StatusBar controls the system status bar appearance
  • Use barStyle="light-content" on dark backgrounds, "dark-content" on light
  • Android has additional props: backgroundColor and translucent
  • Update StatusBar when navigating between screens with different themes

Quick Reference

// Switch
<Switch
  value={isEnabled}
  onValueChange={setIsEnabled}
  trackColor={{ false: '#767577', true: '#81b0ff' }}
  thumbColor={isEnabled ? '#2196F3' : '#f4f3f4'}
  disabled={false}
/>

// ActivityIndicator
<ActivityIndicator 
  size="large"        // 'small' | 'large' | number (Android)
  color="#2196F3" 
  animating={true}
/>

// StatusBar
<StatusBar
  barStyle="dark-content"    // 'dark-content' | 'light-content' | 'default'
  backgroundColor="#fff"     // Android only
  translucent={false}        // Android only
  hidden={false}
  animated
/>

// Imperative API
StatusBar.setBarStyle('light-content', true);
StatusBar.setHidden(true, 'fade');

🎓 Module 3 Complete!

Congratulations! You've completed Module 3: Core Components. You now know how to use all the essential building blocks:

📦
View

Universal container

📝
Text

Typography

🖼️
Image

Visual content

📜
ScrollView

Scrolling content

🛡️
SafeAreaView

Device boundaries

👆
Pressable

Touch handling

⌨️
TextInput

User input

🔘
Switch & More

Utility components

🚀 What's Next?

In Module 4: StyleSheet Deep Dive, you'll master Flexbox layouts, responsive design patterns, platform-specific styling, and organizing styles at scale.

🎉 Module 3 Complete!

You've mastered all the core components of React Native. With these building blocks, you can create any UI imaginable. Next up: making those UIs beautiful with StyleSheet mastery!