🖼️ 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.
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"
/>
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:
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 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-imagefor 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'), andnameprops - 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!