Skip to main content

🖼️ Image: Displaying Visual Content

From local assets to remote URLs — mastering images in React Native

🎯 Learning Objectives

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

  • Display images from local assets and remote URLs
  • Understand the differences between local and network image handling
  • Apply different resizeMode options to control image scaling
  • Style images with borders, shadows, and rounded corners
  • Handle loading states and errors gracefully
  • Use ImageBackground to layer content over images
  • Optimize image performance in your apps

⏱️ Estimated Time: 30-40 minutes

📑 In This Lesson

Image Basics

The Image component is React Native's way of displaying images. It maps to native image views on each platform — UIImageView on iOS and ImageView on Android.

📖 Key Concept

Unlike the web's <img> tag, React Native's Image component has some important differences: remote images require explicit dimensions, and the source prop works differently for local vs network images.

Your First Image

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

export default function BasicImage() {
  return (
    <View style={styles.container}>
      {/* Local image */}
      <Image 
        source={require('./assets/logo.png')}
        style={styles.logo}
      />
      
      {/* Remote image */}
      <Image 
        source={{ uri: 'https://picsum.photos/200/200' }}
        style={styles.remoteImage}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 20,
  },
  logo: {
    width: 100,
    height: 100,
  },
  remoteImage: {
    width: 200,
    height: 200,
  },
});

Notice the key difference: local images use require(), while remote images use an object with a uri property.

Image Source Types 📁 Local Image source = {require( './logo.png' )} ✓ Bundled at build time ✓ Dimensions auto-detected ✓ Cached automatically ✓ Works offline 🌐 Remote Image source = {{ uri : 'https://...' }} ⚠️ Loaded at runtime ⚠️ Requires width/height ⚠️ Needs network connection ✓ Can be cached with libraries

Local and remote images have different syntax and behavior

Local vs Remote Images

Understanding the difference between local and remote image handling is crucial for building reliable apps.

Local Images with require()

Local images are bundled with your app at build time. They're reliable, fast, and work offline:

// Static require - path must be a string literal
<Image source={require('./assets/images/hero.png')} />

// This WON'T work - dynamic paths aren't supported
const imageName = 'hero.png';
<Image source={require(`./assets/images/${imageName}`)} /> // ❌ Error!

⚠️ Static Analysis Limitation

The require() path must be a static string literal. React Native's bundler (Metro) needs to know all image paths at build time. You cannot construct paths dynamically.

Workaround for dynamic local images:

// Create a mapping object
const images = {
  hero: require('./assets/images/hero.png'),
  logo: require('./assets/images/logo.png'),
  avatar: require('./assets/images/avatar.png'),
};

// Use the mapping
<Image source={images[imageName]} />

Automatic Sizing for Local Images

One advantage of local images: React Native can detect their dimensions automatically:

// Local image - dimensions are optional
// React Native will use the image's actual size
<Image source={require('./assets/logo.png')} />

// But you can still override
<Image 
  source={require('./assets/logo.png')}
  style={{ width: 50, height: 50 }}
/>

Remote Images with uri

Remote images are loaded from URLs at runtime:

// Basic remote image
<Image 
  source={{ uri: 'https://example.com/photo.jpg' }}
  style={{ width: 200, height: 200 }}  // REQUIRED!
/>

// With additional options
<Image 
  source={{
    uri: 'https://example.com/photo.jpg',
    headers: {
      Authorization: 'Bearer token123',
    },
    cache: 'force-cache',  // iOS only
  }}
  style={{ width: 200, height: 200 }}
/>

❌ Common Mistake: Missing Dimensions

// This will show NOTHING - remote images need explicit size
<Image source={{ uri: 'https://example.com/photo.jpg' }} />

// ✅ Fixed - always provide width and height
<Image 
  source={{ uri: 'https://example.com/photo.jpg' }}
  style={{ width: 200, height: 200 }}
/>

Remote images have no inherent size until they load. Without dimensions, the Image component renders with 0×0 size.

Source Object Properties

Property Type Description
uri string URL of the image (http://, https://, data:)
headers object HTTP headers to send with the request
width number Width hint for remote images
height number Height hint for remote images
cache string iOS: 'default', 'reload', 'force-cache', 'only-if-cached'
scale number Scale factor (defaults to 1.0)

Base64 Data URIs

You can also display images from base64 encoded data:

// Base64 image (useful for small icons or dynamically generated images)
<Image 
  source={{
    uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...',
  }}
  style={{ width: 50, height: 50 }}
/>

💡 When to Use Base64

  • Very small images (icons under 1KB)
  • Images generated or received from an API as base64
  • Avoiding extra network requests for tiny assets

Avoid base64 for larger images — it increases bundle size and memory usage.

Understanding resizeMode

When an image's aspect ratio doesn't match its container, resizeMode determines how the image scales. This is similar to CSS's object-fit property.

The Five Resize Modes

<Image 
  source={{ uri: imageUrl }}
  style={{ width: 200, height: 200 }}
  resizeMode="cover"  // or "contain", "stretch", "repeat", "center"
/>
resizeMode Options Container: 150×100 | Image: 200×150 (4:3 ratio) cover Fills container, may crop Maintains aspect ratio contain Fits inside, may letterbox Maintains aspect ratio stretch Fills exactly, distorts Ignores aspect ratio center Original size, centered May overflow or be small repeat Tiles to fill (iOS only) Good for patterns Quick Guide: cover = Hero images, backgrounds (most common) contain = Product photos, logos stretch = Rarely used

Different resizeMode options and their visual effects

resizeMode Reference

Mode Behavior Best For
cover Scales to fill container, crops excess. Maintains aspect ratio. Hero images, backgrounds, avatars
contain Scales to fit inside container, may leave empty space. Maintains aspect ratio. Product photos, logos, diagrams
stretch Stretches to fill container exactly. Ignores aspect ratio. Gradients, patterns (rarely photos)
center Centers at original size. No scaling. Icons at exact size, pixel art
repeat Tiles the image. iOS only. Background patterns, textures

Common Usage Examples

// Avatar - circle crop with cover
<Image 
  source={{ uri: user.avatarUrl }}
  style={styles.avatar}
  resizeMode="cover"
/>

// Product image - contain to show full product
<Image 
  source={{ uri: product.imageUrl }}
  style={styles.productImage}
  resizeMode="contain"
/>

// Hero banner - cover for full-bleed
<Image 
  source={require('./assets/hero.jpg')}
  style={styles.heroBanner}
  resizeMode="cover"
/>

const styles = StyleSheet.create({
  avatar: {
    width: 60,
    height: 60,
    borderRadius: 30,  // Makes it circular
  },
  productImage: {
    width: '100%',
    height: 300,
    backgroundColor: '#f5f5f5',  // Visible in letterbox areas
  },
  heroBanner: {
    width: '100%',
    height: 200,
  },
});

Styling Images

Images accept many of the same style properties as View, plus some image-specific ones. Let's explore common styling patterns.

Basic Dimensions

// Fixed dimensions
<Image 
  source={source}
  style={{ width: 200, height: 150 }}
/>

// Percentage width (relative to parent)
<Image 
  source={source}
  style={{ width: '100%', height: 200 }}
/>

// Aspect ratio (modern RN)
<Image 
  source={source}
  style={{ width: '100%', aspectRatio: 16/9 }}
/>

✅ Pro Tip: aspectRatio

The aspectRatio style property is incredibly useful. Set one dimension and the aspect ratio, and React Native calculates the other dimension automatically:

// Width is 100%, height auto-calculated for 16:9
style={{ width: '100%', aspectRatio: 16/9 }}

// Height is 200, width auto-calculated for square
style={{ height: 200, aspectRatio: 1 }}

Rounded Corners and Circles

// Rounded corners
<Image 
  source={source}
  style={{
    width: 200,
    height: 150,
    borderRadius: 12,
  }}
/>

// Circular image (avatar pattern)
<Image 
  source={source}
  style={{
    width: 100,
    height: 100,
    borderRadius: 50,  // Half of width/height
  }}
/>

// Individual corner radii
<Image 
  source={source}
  style={{
    width: 200,
    height: 150,
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    borderBottomLeftRadius: 0,
    borderBottomRightRadius: 0,
  }}
/>

Borders

<Image 
  source={source}
  style={{
    width: 150,
    height: 150,
    borderRadius: 75,
    borderWidth: 3,
    borderColor: '#2196F3',
  }}
/>

Shadows (iOS)

Images can have shadows, but with a caveat for circular images:

// Shadow on rectangular image
<Image 
  source={source}
  style={{
    width: 200,
    height: 150,
    borderRadius: 12,
    // iOS shadows
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
  }}
/>

// For Android, wrap in a View with elevation
<View style={{
  elevation: 5,  // Android shadow
  borderRadius: 12,
}}>
  <Image 
    source={source}
    style={{
      width: 200,
      height: 150,
      borderRadius: 12,
    }}
  />
</View>

⚠️ Shadows on Circular Images

On iOS, shadows on images with borderRadius can behave unexpectedly. The shadow may follow the original rectangular bounds. For reliable circular shadows, wrap the image in a View:

<View style={styles.avatarShadow}>
  <Image source={source} style={styles.avatar} />
</View>

const styles = StyleSheet.create({
  avatarShadow: {
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  avatar: {
    width: 80,
    height: 80,
    borderRadius: 40,
  },
});

Opacity and Tinting

// Reduced opacity
<Image 
  source={source}
  style={{ width: 200, height: 150, opacity: 0.7 }}
/>

// Tint color (recolors the image)
// Useful for icons
<Image 
  source={require('./assets/icon.png')}
  style={{ 
    width: 24, 
    height: 24,
    tintColor: '#2196F3',  // Icon becomes blue
  }}
/>

The tintColor property is especially useful for icons — you can have one white or black icon and tint it to any color:

tintColor transforms icon color Original #2196F3 #4CAF50 #F44336 #9C27B0

One icon asset, multiple colors with tintColor

Loading States and Errors

Remote images take time to load and can fail. React Native's Image provides callbacks to handle these states.

Image Event Callbacks

<Image 
  source={{ uri: imageUrl }}
  style={styles.image}
  onLoadStart={() => console.log('Loading started')}
  onLoad={(event) => {
    const { width, height } = event.nativeEvent.source;
    console.log(`Loaded: ${width}x${height}`);
  }}
  onLoadEnd={() => console.log('Loading finished')}
  onError={(error) => console.log('Error:', error.nativeEvent.error)}
/>

Loading Indicator Pattern

A common pattern is showing a placeholder or spinner while the image loads:

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

function ImageWithLoader({ uri, style }) {
  const [isLoading, setIsLoading] = useState(true);
  const [hasError, setHasError] = useState(false);

  return (
    <View style={[styles.container, style]}>
      {isLoading && (
        <View style={styles.loaderContainer}>
          <ActivityIndicator size="large" color="#2196F3" />
        </View>
      )}
      
      {hasError ? (
        <View style={styles.errorContainer}>
          <Text>Failed to load image</Text>
        </View>
      ) : (
        <Image 
          source={{ uri }}
          style={StyleSheet.absoluteFill}
          resizeMode="cover"
          onLoadEnd={() => setIsLoading(false)}
          onError={() => {
            setIsLoading(false);
            setHasError(true);
          }}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#f0f0f0',
    overflow: 'hidden',
  },
  loaderContainer: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
  },
  errorContainer: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ffebee',
  },
});

Fallback Image Pattern

Show a fallback image when the primary image fails:

import { useState } from 'react';
import { Image } from 'react-native';

const FALLBACK_AVATAR = require('./assets/default-avatar.png');

function Avatar({ uri, size = 50 }) {
  const [source, setSource] = useState({ uri });

  return (
    <Image 
      source={source}
      style={{
        width: size,
        height: size,
        borderRadius: size / 2,
      }}
      onError={() => setSource(FALLBACK_AVATAR)}
    />
  );
}

defaultSource (iOS)

On iOS, you can show a local placeholder while the remote image loads:

// iOS only - shows placeholder until remote loads
<Image 
  source={{ uri: 'https://example.com/photo.jpg' }}
  defaultSource={require('./assets/placeholder.png')}
  style={{ width: 200, height: 200 }}
/>

⚠️ Platform Limitation

defaultSource only works on iOS. For cross-platform placeholder support, use the loading state pattern shown above, or consider a library like expo-image.

ImageBackground Component

ImageBackground is a specialized component that lets you layer other content on top of an image. It's React Native's answer to CSS's background-image.

Basic Usage

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

function HeroSection() {
  return (
    <ImageBackground
      source={require('./assets/hero-bg.jpg')}
      style={styles.hero}
      resizeMode="cover"
    >
      <View style={styles.overlay}>
        <Text style={styles.title}>Welcome</Text>
        <Text style={styles.subtitle}>to our app</Text>
      </View>
    </ImageBackground>
  );
}

const styles = StyleSheet.create({
  hero: {
    width: '100%',
    height: 300,
    justifyContent: 'center',
    alignItems: 'center',
  },
  overlay: {
    backgroundColor: 'rgba(0, 0, 0, 0.4)',
    padding: 20,
    borderRadius: 10,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: 'white',
  },
  subtitle: {
    fontSize: 18,
    color: 'white',
  },
});
ImageBackground Structure Welcome to our app Children (any components) Rendered on top of image Background image source prop ImageBackground works like View + Image combined

ImageBackground renders children on top of the image

Common Patterns

🌅 Card with Image Header

<ImageBackground
  source={{ uri: article.imageUrl }}
  style={styles.cardHeader}
  imageStyle={{ borderTopLeftRadius: 12, 
                borderTopRightRadius: 12 }}
>
  <View style={styles.categoryBadge}>
    <Text>{article.category}</Text>
  </View>
</ImageBackground>

🎨 Gradient Overlay

<ImageBackground 
  source={source} 
  style={styles.container}
>
  <LinearGradient
    colors={['transparent', 'rgba(0,0,0,0.8)']}
    style={styles.gradient}
  >
    <Text style={styles.title}>{title}</Text>
  </LinearGradient>
</ImageBackground>

imageStyle Prop

The imageStyle prop lets you style the underlying image separately from the container:

<ImageBackground
  source={source}
  style={styles.container}      // Container styles (dimensions, margin, etc.)
  imageStyle={styles.image}     // Image-specific styles (borderRadius, opacity)
  resizeMode="cover"
>
  {children}
</ImageBackground>

const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: 200,
    justifyContent: 'flex-end',
    padding: 16,
  },
  image: {
    borderRadius: 12,
    opacity: 0.9,
  },
});

Performance Optimization

Images are often the biggest performance bottleneck in mobile apps. Let's explore strategies for keeping your app fast.

Image Sizing

Always serve appropriately sized images. Loading a 4000×3000 image for a 200×200 thumbnail wastes bandwidth and memory.

// If your API supports image sizing, use it!
const thumbnailUrl = `${baseUrl}/image.jpg?width=400&height=400`;
const fullSizeUrl = `${baseUrl}/image.jpg`;

// Show thumbnail in list, full size in detail view
<Image source={{ uri: thumbnailUrl }} style={{ width: 200, height: 200 }} />

Caching with expo-image

React Native's built-in Image component has limited caching. For production apps, consider expo-image:

# Install expo-image
npx expo install expo-image
import { Image } from 'expo-image';

// expo-image with automatic caching
<Image 
  source={{ uri: imageUrl }}
  style={{ width: 200, height: 200 }}
  contentFit="cover"  // Similar to resizeMode
  placeholder={blurhash}  // Blur placeholder
  transition={200}  // Fade-in animation (ms)
  cachePolicy="memory-disk"  // Cache aggressively
/>

✅ Why expo-image?

  • Better caching — Memory and disk caching out of the box
  • Blurhash placeholders — Show blurred preview while loading
  • Smooth transitions — Fade-in when image loads
  • Better performance — Optimized native implementation
  • SVG support — Render SVG images directly

Lazy Loading in Lists

In long lists, only load images that are visible or about to be visible:

import { FlatList, Image, View } from 'react-native';

function ImageList({ images }) {
  return (
    <FlatList
      data={images}
      keyExtractor={(item) => item.id}
      // Only render items near the viewport
      windowSize={5}  // Render 5 screens worth
      initialNumToRender={10}
      maxToRenderPerBatch={10}
      removeClippedSubviews={true}  // Unmount off-screen items
      renderItem={({ item }) => (
        <Image 
          source={{ uri: item.url }}
          style={{ width: '100%', height: 200 }}
        />
      )}
    />
  );
}

Prefetching Images

Load images before they're needed:

import { Image } from 'react-native';

// Prefetch a single image
Image.prefetch('https://example.com/image.jpg');

// Prefetch multiple images
const imageUrls = [
  'https://example.com/image1.jpg',
  'https://example.com/image2.jpg',
  'https://example.com/image3.jpg',
];

Promise.all(imageUrls.map(url => Image.prefetch(url)))
  .then(() => console.log('All images prefetched'))
  .catch(err => console.log('Prefetch error:', err));

Performance Checklist

💡 Image Performance Tips

  • ✅ Use appropriately sized images (not larger than needed)
  • ✅ Prefer JPEG for photos, PNG for graphics with transparency
  • ✅ Consider WebP format for better compression
  • ✅ Use expo-image for automatic caching
  • ✅ Implement lazy loading in lists
  • ✅ Prefetch images you know will be needed soon
  • ✅ Show placeholders during loading
  • ✅ Handle errors gracefully with fallback images

Image Accessibility

Making images accessible ensures that screen reader users understand your visual content. In React Native, you use the same accessibility props as View.

Accessible Labels

// Informative image - describe the content
<Image 
  source={{ uri: product.imageUrl }}
  style={styles.productImage}
  accessible={true}
  accessibilityLabel={`Product photo: ${product.name}`}
/>

// Decorative image - hide from screen readers
<Image 
  source={require('./assets/decoration.png')}
  style={styles.decoration}
  accessible={false}
  accessibilityRole="none"
/>

Types of Images

Image Type Accessibility Approach Example
Informative Describe the content or message "Chart showing sales growth of 25%"
Functional Describe the action it performs "Navigate to home screen"
Decorative Hide from screen readers Background patterns, visual flourishes
Complex Brief label + longer description Infographics, detailed diagrams

Avatar and User Images

// User avatar with name context
<Image 
  source={{ uri: user.avatar }}
  style={styles.avatar}
  accessible={true}
  accessibilityLabel={`${user.name}'s profile photo`}
/>

// Current user's avatar (in navigation, for example)
<Image 
  source={{ uri: currentUser.avatar }}
  style={styles.avatar}
  accessible={true}
  accessibilityLabel="Your profile photo"
  accessibilityHint="Double tap to open profile settings"
/>

Icons as Images

When using image icons (rather than vector icons), ensure they're properly labeled:

// Tappable icon image
<Pressable 
  onPress={handleLike}
  accessible={true}
  accessibilityRole="button"
  accessibilityLabel={isLiked ? "Unlike this post" : "Like this post"}
>
  <Image 
    source={isLiked ? heartFilled : heartOutline}
    style={[styles.icon, { tintColor: isLiked ? 'red' : 'gray' }]}
    accessible={false}  // Parent handles accessibility
  />
</Pressable>

✅ Accessibility Best Practices

  • Every informative image needs an accessibilityLabel
  • Decorative images should be hidden with accessibilityRole="none"
  • Don't start labels with "Image of..." — screen readers already announce it's an image
  • Be concise but descriptive
  • Test with VoiceOver (iOS) and TalkBack (Android)

Hands-On Exercises

Practice makes perfect! These exercises will help you master the Image component.

Exercise 1: Avatar Component

Goal: Create a reusable Avatar component with multiple size variants.

Requirements:

  • Accept uri, size ('small' | 'medium' | 'large'), and name props
  • Sizes: small=40, medium=60, large=100
  • Circular shape with light border
  • Show initials on a colored background if image fails to load
  • Proper accessibility label with the user's name
💡 Hint

Use onError to detect failed loads. You can create a helper function to extract initials from a name (first letter of first and last name).

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

const SIZES = {
  small: 40,
  medium: 60,
  large: 100,
};

const COLORS = ['#f44336', '#2196F3', '#4caf50', '#ff9800', '#9c27b0'];

function getInitials(name: string): string {
  const parts = name.trim().split(' ');
  if (parts.length >= 2) {
    return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
  }
  return name.slice(0, 2).toUpperCase();
}

function getColorFromName(name: string): string {
  const index = name.length % COLORS.length;
  return COLORS[index];
}

interface AvatarProps {
  uri?: string;
  name: string;
  size?: 'small' | 'medium' | 'large';
}

export default function Avatar({ uri, name, size = 'medium' }: AvatarProps) {
  const [hasError, setHasError] = useState(false);
  const dimension = SIZES[size];
  
  if (!uri || hasError) {
    return (
      <View 
        style={[
          styles.fallback,
          {
            width: dimension,
            height: dimension,
            borderRadius: dimension / 2,
            backgroundColor: getColorFromName(name),
          },
        ]}
        accessible={true}
        accessibilityLabel={`${name}'s avatar`}
      >
        <Text style={[styles.initials, { fontSize: dimension * 0.4 }]}>
          {getInitials(name)}
        </Text>
      </View>
    );
  }

  return (
    <Image 
      source={{ uri }}
      style={[
        styles.avatar,
        {
          width: dimension,
          height: dimension,
          borderRadius: dimension / 2,
        },
      ]}
      onError={() => setHasError(true)}
      accessible={true}
      accessibilityLabel={`${name}'s profile photo`}
    />
  );
}

const styles = StyleSheet.create({
  avatar: {
    borderWidth: 2,
    borderColor: '#eee',
  },
  fallback: {
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 2,
    borderColor: '#eee',
  },
  initials: {
    color: 'white',
    fontWeight: 'bold',
  },
});

Exercise 2: Image Gallery Grid

Goal: Create a 3-column image gallery grid.

Requirements:

  • Display images in a 3-column grid layout
  • Square thumbnails with small gaps between them
  • Use resizeMode="cover" for consistent appearance
  • Show a loading indicator while images load
💡 Hint

Use a View with flexDirection: 'row' and flexWrap: 'wrap'. Calculate each image's width as roughly 1/3 of the container minus gaps.

✅ Solution
import { useState } from 'react';
import { View, Image, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';

const { width: screenWidth } = Dimensions.get('window');
const GAP = 4;
const COLUMNS = 3;
const IMAGE_SIZE = (screenWidth - GAP * (COLUMNS + 1)) / COLUMNS;

interface GalleryImageProps {
  uri: string;
}

function GalleryImage({ uri }: GalleryImageProps) {
  const [isLoading, setIsLoading] = useState(true);

  return (
    <View style={styles.imageContainer}>
      {isLoading && (
        <View style={styles.loader}>
          <ActivityIndicator size="small" color="#2196F3" />
        </View>
      )}
      <Image 
        source={{ uri }}
        style={styles.image}
        resizeMode="cover"
        onLoadEnd={() => setIsLoading(false)}
      />
    </View>
  );
}

interface ImageGalleryProps {
  images: { id: string; uri: string }[];
}

export default function ImageGallery({ images }: ImageGalleryProps) {
  return (
    <View style={styles.gallery}>
      {images.map((image) => (
        <GalleryImage key={image.id} uri={image.uri} />
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  gallery: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    padding: GAP,
  },
  imageContainer: {
    width: IMAGE_SIZE,
    height: IMAGE_SIZE,
    margin: GAP / 2,
    backgroundColor: '#f0f0f0',
  },
  image: {
    width: '100%',
    height: '100%',
  },
  loader: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

Exercise 3: Hero Card with ImageBackground

Goal: Create a hero card component with an image background and text overlay.

Requirements:

  • Full-width card with 200px height
  • Image background with rounded corners
  • Semi-transparent dark overlay
  • Title (white, bold) and subtitle (white, lighter) at the bottom
  • Tap the card to trigger an onPress callback
💡 Hint

Wrap ImageBackground in a Pressable. Use imageStyle for the rounded corners on the image. Position text at the bottom with justifyContent: 'flex-end'.

✅ Solution
import { ImageBackground, Pressable, View, Text, StyleSheet } from 'react-native';

interface HeroCardProps {
  imageUri: string;
  title: string;
  subtitle: string;
  onPress: () => void;
}

export default function HeroCard({ imageUri, title, subtitle, onPress }: HeroCardProps) {
  return (
    <Pressable 
      onPress={onPress}
      style={({ pressed }) => [
        styles.pressable,
        pressed && styles.pressed,
      ]}
    >
      <ImageBackground
        source={{ uri: imageUri }}
        style={styles.background}
        imageStyle={styles.image}
        resizeMode="cover"
      >
        <View style={styles.overlay}>
          <View style={styles.textContainer}>
            <Text style={styles.title}>{title}</Text>
            <Text style={styles.subtitle}>{subtitle}</Text>
          </View>
        </View>
      </ImageBackground>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  pressable: {
    marginHorizontal: 16,
    marginVertical: 8,
  },
  pressed: {
    opacity: 0.9,
    transform: [{ scale: 0.98 }],
  },
  background: {
    width: '100%',
    height: 200,
  },
  image: {
    borderRadius: 16,
  },
  overlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.35)',
    borderRadius: 16,
    justifyContent: 'flex-end',
  },
  textContainer: {
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: 'white',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: 'rgba(255, 255, 255, 0.9)',
  },
});

Challenge: Zoomable Image

🏆 Bonus Challenge

Goal: Create an image that shows a larger version in a modal when tapped.

Features:

  • Thumbnail image that's tappable
  • Full-screen modal with the image when tapped
  • Close button or tap to dismiss the modal
  • Image should use resizeMode="contain" in the modal
✅ Solution
import { useState } from 'react';
import { 
  Image, View, Pressable, Modal, 
  StyleSheet, SafeAreaView 
} from 'react-native';

interface ZoomableImageProps {
  uri: string;
  thumbnailStyle: object;
}

export default function ZoomableImage({ uri, thumbnailStyle }: ZoomableImageProps) {
  const [isModalVisible, setIsModalVisible] = useState(false);

  return (
    <>
      <Pressable onPress={() => setIsModalVisible(true)}>
        <Image 
          source={{ uri }}
          style={thumbnailStyle}
          resizeMode="cover"
        />
      </Pressable>

      <Modal
        visible={isModalVisible}
        animationType="fade"
        transparent={true}
        onRequestClose={() => setIsModalVisible(false)}
      >
        <Pressable 
          style={styles.modalContainer}
          onPress={() => setIsModalVisible(false)}
        >
          <SafeAreaView style={styles.safeArea}>
            <Pressable 
              style={styles.closeButton}
              onPress={() => setIsModalVisible(false)}
            >
              <Text style={styles.closeText}>✕</Text>
            </Pressable>
            <Image 
              source={{ uri }}
              style={styles.fullImage}
              resizeMode="contain"
            />
          </SafeAreaView>
        </Pressable>
      </Modal>
    </>
  );
}

const styles = StyleSheet.create({
  modalContainer: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.95)',
  },
  safeArea: {
    flex: 1,
  },
  closeButton: {
    position: 'absolute',
    top: 50,
    right: 20,
    zIndex: 10,
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: 'rgba(255, 255, 255, 0.2)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  closeText: {
    color: 'white',
    fontSize: 20,
    fontWeight: 'bold',
  },
  fullImage: {
    flex: 1,
    width: '100%',
  },
});

Summary

🎉 Key Takeaways

  • Local images use require() — path must be a static string literal
  • Remote images need explicit dimensions — they won't display without width and height
  • resizeMode controls scaling — use 'cover' for backgrounds, 'contain' for products
  • aspectRatio simplifies sizing — set one dimension and let the other auto-calculate
  • Handle loading and errors — show placeholders and fallbacks for better UX
  • ImageBackground layers content — React Native's answer to background-image
  • Consider expo-image for production — better caching, blurhash, and transitions
  • Accessibility labels are essential — describe informative images, hide decorative ones

Quick Reference

import { Image, ImageBackground } from 'react-native';

// Local image
<Image source={require('./image.png')} />

// Remote image (dimensions required!)
<Image 
  source={{ uri: 'https://...' }}
  style={{ width: 200, height: 200 }}
  resizeMode="cover"
/>

// With loading/error handling
<Image 
  source={{ uri }}
  onLoadStart={() => {}}
  onLoad={() => {}}
  onError={() => {}}
/>

// Image background
<ImageBackground source={source} style={styles.bg}>
  <Text>Overlay content</Text>
</ImageBackground>

// Circular avatar
style={{ 
  width: 60, 
  height: 60, 
  borderRadius: 30 
}}

🚀 What's Next?

Now that you've mastered displaying content, we'll explore ScrollView — how to handle content that exceeds the screen size and create scrollable interfaces.

🖼️ Images Mastered!

You can now display images from any source, style them beautifully, handle loading states, and ensure they're accessible. Your apps are going to look amazing!