📜 ScrollView: When Content Overflows
Making content scrollable when it exceeds the screen
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Understand why View doesn't scroll and when to use ScrollView
- Implement vertical and horizontal scrolling
- Configure scroll behavior with key props
- Handle scroll events programmatically
- Build common UI patterns like pull-to-refresh
- Recognize when ScrollView is NOT the right choice
⏱️ Estimated Time: 25-35 minutes
📑 In This Lesson
Why ScrollView?
In React Native, View components don't scroll. If content exceeds the container's bounds, it simply gets clipped — users can't see it, and there's no way to scroll to it.
📖 Key Difference from Web
On the web, you can make any element scrollable with overflow: scroll. In React Native, scrolling requires a dedicated component — ScrollView.
The Problem
// ❌ Content is clipped, not scrollable
<View style={{ height: 300 }}>
<Text>Paragraph 1...</Text>
<Text>Paragraph 2...</Text>
<Text>Paragraph 3...</Text>
{/* If this exceeds 300px, users can't see the rest! */}
</View>
The Solution
// ✅ Content scrolls when it exceeds bounds
<ScrollView style={{ height: 300 }}>
<Text>Paragraph 1...</Text>
<Text>Paragraph 2...</Text>
<Text>Paragraph 3...</Text>
{/* Users can scroll to see all content */}
</ScrollView>
View clips overflow content; ScrollView makes it accessible through scrolling
What ScrollView Does
ScrollView creates a scrollable container that:
- Renders all its children immediately
- Allows users to scroll vertically (default) or horizontally
- Bounces at the edges (iOS) for a native feel
- Supports momentum scrolling
- Can respond to scroll events programmatically
Basic Usage
Using ScrollView is straightforward — wrap your content and you're scrolling:
import { ScrollView, View, Text, StyleSheet } from 'react-native';
export default function ArticleScreen() {
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Article Title</Text>
<Text style={styles.body}>
{longArticleText}
</Text>
<View style={styles.spacer} />
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
padding: 16,
},
body: {
fontSize: 16,
lineHeight: 24,
padding: 16,
},
spacer: {
height: 40, // Extra space at bottom
},
});
style vs contentContainerStyle
ScrollView has two style props that serve different purposes:
| Prop | What It Styles | Common Uses |
|---|---|---|
style |
The ScrollView container itself | Background color, flex, margins |
contentContainerStyle |
The inner content wrapper | Padding, alignment, gap |
<ScrollView
style={styles.scrollView} // Outer container
contentContainerStyle={styles.content} // Inner content
>
{children}
</ScrollView>
const styles = StyleSheet.create({
scrollView: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 20,
gap: 16,
// For centering content vertically when it's shorter than screen:
// flexGrow: 1,
// justifyContent: 'center',
},
});
⚠️ Common Mistake: flex: 1 on contentContainerStyle
Don't use flex: 1 on contentContainerStyle — it can prevent scrolling by collapsing the content height. Use flexGrow: 1 instead if you need the content to expand:
// ❌ Can break scrolling
contentContainerStyle={{ flex: 1 }}
// ✅ Allows content to grow but still scroll
contentContainerStyle={{ flexGrow: 1 }}
style affects the outer frame; contentContainerStyle affects the scrollable content area
Horizontal Scrolling
By default, ScrollView scrolls vertically. Add the horizontal prop for horizontal scrolling:
import { ScrollView, View, Text, StyleSheet } from 'react-native';
function CategoryPills({ categories }) {
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.pillContainer}
>
{categories.map((category) => (
<View key={category.id} style={styles.pill}>
<Text style={styles.pillText}>{category.name}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
pillContainer: {
paddingHorizontal: 16,
paddingVertical: 12,
gap: 8,
},
pill: {
backgroundColor: '#e3f2fd',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
},
pillText: {
color: '#1976d2',
fontWeight: '500',
},
});
Horizontal Image Gallery
function ImageGallery({ images }) {
return (
<ScrollView
horizontal
pagingEnabled // Snap to each image
showsHorizontalScrollIndicator={false}
>
{images.map((image, index) => (
<Image
key={index}
source={{ uri: image.url }}
style={styles.galleryImage}
resizeMode="cover"
/>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
galleryImage: {
width: Dimensions.get('window').width, // Full screen width
height: 300,
},
});
✅ Pro Tip: pagingEnabled
Use pagingEnabled to create a carousel or paging effect. The ScrollView will snap to multiples of its width, making each "page" stop exactly in view.
Horizontal + Vertical Scrolling
You can nest ScrollViews for both directions:
// Vertical main scroll, with horizontal rows inside
<ScrollView style={styles.container}>
<Text style={styles.sectionTitle}>Trending</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{trendingItems.map(item => <Card key={item.id} {...item} />)}
</ScrollView>
<Text style={styles.sectionTitle}>New Releases</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{newReleases.map(item => <Card key={item.id} {...item} />)}
</ScrollView>
</ScrollView>
Key Props
ScrollView has many props to customize its behavior. Here are the most important ones:
Scroll Behavior Props
| Prop | Type | Description |
|---|---|---|
horizontal |
boolean | Scroll horizontally instead of vertically |
pagingEnabled |
boolean | Snap to pages (multiples of scroll view size) |
scrollEnabled |
boolean | Enable/disable scrolling (default: true) |
bounces |
boolean | iOS: Bounce at edges (default: true) |
overScrollMode |
string | Android: 'auto', 'always', 'never' |
scrollEventThrottle |
number | How often onScroll fires (ms) |
decelerationRate |
'normal' | 'fast' | number | How quickly scrolling decelerates |
Visual Props
| Prop | Type | Description |
|---|---|---|
showsVerticalScrollIndicator |
boolean | Show vertical scrollbar (default: true) |
showsHorizontalScrollIndicator |
boolean | Show horizontal scrollbar (default: true) |
indicatorStyle |
'default' | 'black' | 'white' | iOS: Scrollbar color |
contentInset |
object | iOS: Inset from edges {top, left, bottom, right} |
Common Configurations
// Standard vertical scroll (article, settings)
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 16 }}
showsVerticalScrollIndicator={true}
>
// Horizontal carousel with paging
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
>
// Modal content with bounce disabled
<ScrollView
bounces={false}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ padding: 20 }}
>
// Form with keyboard handling
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
contentContainerStyle={{ padding: 16 }}
>
Scroll Events
ScrollView provides callbacks to respond to scroll actions. This enables features like scroll-based animations, lazy loading, and scroll position tracking.
Basic onScroll
import { ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
function ScrollTracker() {
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
console.log('Scroll position:', contentOffset.y);
console.log('Content height:', contentSize.height);
console.log('Visible height:', layoutMeasurement.height);
// Calculate scroll percentage
const scrollPercentage =
contentOffset.y / (contentSize.height - layoutMeasurement.height);
console.log('Scroll %:', Math.round(scrollPercentage * 100));
};
return (
<ScrollView
onScroll={handleScroll}
scrollEventThrottle={16} // ~60fps for smooth tracking
>
{/* Content */}
</ScrollView>
);
}
💡 scrollEventThrottle
The scrollEventThrottle prop controls how often onScroll fires. Lower values = more frequent updates:
- 16ms — ~60fps, smoothest but most CPU intensive
- 100ms — Good balance for most use cases
- Not set — Only fires once at end of scroll (iOS)
Scroll Event Data
The scroll event provides these useful values:
event.nativeEvent = {
contentOffset: {
x: number, // Horizontal scroll position
y: number, // Vertical scroll position
},
contentSize: {
width: number, // Total content width
height: number, // Total content height
},
layoutMeasurement: {
width: number, // Visible viewport width
height: number, // Visible viewport height
},
// Plus velocity, zoomScale, etc.
}
Scroll events provide position, content size, and viewport measurements
Other Scroll Callbacks
<ScrollView
// Fired when scrolling starts
onScrollBeginDrag={() => console.log('Started scrolling')}
// Fired when user lifts finger (momentum may continue)
onScrollEndDrag={() => console.log('Finger lifted')}
// Fired when scroll completely stops (including momentum)
onMomentumScrollEnd={() => console.log('Scroll finished')}
// Fired when momentum scrolling begins
onMomentumScrollBegin={() => console.log('Momentum started')}
>
Programmatic Scrolling
You can scroll to specific positions using refs:
import { useRef } from 'react';
import { ScrollView, Button, View } from 'react-native';
function ScrollableContent() {
const scrollViewRef = useRef<ScrollView>(null);
const scrollToTop = () => {
scrollViewRef.current?.scrollTo({ y: 0, animated: true });
};
const scrollToBottom = () => {
scrollViewRef.current?.scrollToEnd({ animated: true });
};
const scrollToPosition = () => {
scrollViewRef.current?.scrollTo({ y: 500, animated: true });
};
return (
<View style={{ flex: 1 }}>
<View style={styles.buttons}>
<Button title="Top" onPress={scrollToTop} />
<Button title="Bottom" onPress={scrollToBottom} />
</View>
<ScrollView ref={scrollViewRef}>
{/* Long content */}
</ScrollView>
</View>
);
}
Detecting End of Scroll
Useful for infinite scrolling or "load more" patterns:
const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }) => {
const paddingToBottom = 20;
return layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom;
};
<ScrollView
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
console.log('Near bottom - load more!');
loadMoreContent();
}
}}
scrollEventThrottle={400}
>
Pull-to-Refresh
Pull-to-refresh is a common mobile pattern where users pull down at the top of a list to refresh content. ScrollView supports this natively through the RefreshControl component.
Basic Implementation
import { useState, useCallback } from 'react';
import { ScrollView, RefreshControl, Text, StyleSheet } from 'react-native';
function RefreshableList() {
const [refreshing, setRefreshing] = useState(false);
const [data, setData] = useState(['Item 1', 'Item 2', 'Item 3']);
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Update data
setData(prev => [`New Item ${Date.now()}`, ...prev]);
} finally {
setRefreshing(false);
}
}, []);
return (
<ScrollView
contentContainerStyle={styles.container}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
>
{data.map((item, index) => (
<Text key={index} style={styles.item}>{item}</Text>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
item: {
padding: 16,
backgroundColor: '#f5f5f5',
marginBottom: 8,
borderRadius: 8,
},
});
Customizing RefreshControl
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
// Colors
colors={['#2196F3', '#4caf50', '#ff9800']} // Android: rotating colors
tintColor="#2196F3" // iOS: spinner color
// Title (iOS only)
title="Pull to refresh"
titleColor="#666"
// Progress position (Android)
progressBackgroundColor="#fff"
progressViewOffset={20}
/>
✅ Best Practices for Pull-to-Refresh
- Always provide visual feedback (the spinner) during refresh
- Set
refreshingto false when done, even on error - Use
useCallbackto prevent unnecessary re-renders - Consider showing a toast or message after successful refresh
- Add optimistic updates for better perceived performance
Keyboard Handling
When forms are inside a ScrollView, you need to handle the keyboard properly to ensure inputs remain visible.
keyboardDismissMode
Controls when the keyboard dismisses during scrolling:
<ScrollView
keyboardDismissMode="on-drag" // Dismiss when user starts scrolling
>
{/* Form inputs */}
</ScrollView>
| Value | Behavior |
|---|---|
'none' |
Keyboard stays open during scroll (default) |
'on-drag' |
Dismiss when scrolling starts |
'interactive' |
iOS: Drag down to dismiss interactively |
keyboardShouldPersistTaps
Controls whether tapping outside an input dismisses the keyboard or activates the tapped element:
<ScrollView
keyboardShouldPersistTaps="handled"
>
<TextInput placeholder="Name" />
<TextInput placeholder="Email" />
<Button title="Submit" onPress={handleSubmit} />
</ScrollView>
| Value | Behavior |
|---|---|
'never' |
Tapping outside dismisses keyboard; tap is ignored (default) |
'always' |
Keyboard stays; tap activates the element |
'handled' |
If tap is on a Pressable/Button, activate it; otherwise dismiss |
💡 Recommended for Forms
For most forms, use this combination:
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
>
This lets users tap buttons while keyboard is open, but also dismiss by scrolling.
KeyboardAvoidingView
For forms near the bottom of the screen, wrap ScrollView in KeyboardAvoidingView:
import { KeyboardAvoidingView, ScrollView, Platform } from 'react-native';
function FormScreen() {
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
>
<ScrollView
contentContainerStyle={{ padding: 16 }}
keyboardShouldPersistTaps="handled"
>
<TextInput placeholder="Field 1" />
<TextInput placeholder="Field 2" />
<TextInput placeholder="Field 3" />
{/* More fields... */}
</ScrollView>
</KeyboardAvoidingView>
);
}
When NOT to Use ScrollView
ScrollView has a critical limitation: it renders all children at once. For long lists, this creates serious performance problems.
❌ Don't Use ScrollView For Long Lists
// ❌ BAD - Renders ALL 1000 items immediately
<ScrollView>
{items.map(item => (
<ListItem key={item.id} data={item} />
))}
</ScrollView>
If you have 1000 items, ScrollView creates 1000 component instances at once, consuming massive memory and causing significant lag.
The Problem Visualized
flowchart LR
subgraph ScrollView["ScrollView (All at once)"]
A["Render Item 1"] --> B["Render Item 2"]
B --> C["Render Item 3"]
C --> D["..."]
D --> E["Render Item 1000"]
end
subgraph FlatList["FlatList (Windowed)"]
F["Render visible items only"]
G["~10-20 items in memory"]
end
ScrollView --> H["⚠️ 1000 items in memory"]
FlatList --> I["✓ ~20 items in memory"]
style H fill:#ffebee,stroke:#f44336
style I fill:#e8f5e9,stroke:#4caf50
ScrollView vs FlatList memory usage for large lists
Use FlatList Instead
import { FlatList } from 'react-native';
// ✅ GOOD - Only renders visible items
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ListItem data={item} />}
/>
When to Use Each
| Use ScrollView When: | Use FlatList/SectionList When: |
|---|---|
| Content is short/limited (settings, forms) | Lists with many items (feeds, search results) |
| Mixed content types (article with images) | Homogeneous list items |
| Known, small number of children | Dynamic or large data sets |
| Horizontal carousels with few items | Infinite scroll / pagination |
✅ Rule of Thumb
- Under 20-30 items: ScrollView is fine
- 30+ items or dynamic list: Use FlatList
- Grouped data: Use SectionList
We'll cover FlatList in detail in Module 5: Lists and Performance.
Hands-On Exercises
Practice makes perfect! These exercises will help you master ScrollView.
Exercise 1: Settings Screen
Goal: Create a scrollable settings screen with sections.
Requirements:
- Full-screen ScrollView with light gray background
- Multiple sections: "Account", "Notifications", "Privacy"
- Each section has a title and 3-4 setting rows
- Setting rows should have white background with subtle borders
- Proper padding and spacing throughout
💡 Hint
Use contentContainerStyle for padding. Create reusable SettingRow and SectionTitle components for consistency.
✅ Solution
import { ScrollView, View, Text, StyleSheet } from 'react-native';
function SectionTitle({ title }: { title: string }) {
return <Text style={styles.sectionTitle}>{title}</Text>;
}
function SettingRow({ label }: { label: string }) {
return (
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>{label}</Text>
<Text style={styles.chevron}>›</Text>
</View>
);
}
export default function SettingsScreen() {
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
>
<SectionTitle title="Account" />
<View style={styles.section}>
<SettingRow label="Profile" />
<SettingRow label="Email" />
<SettingRow label="Password" />
<SettingRow label="Linked Accounts" />
</View>
<SectionTitle title="Notifications" />
<View style={styles.section}>
<SettingRow label="Push Notifications" />
<SettingRow label="Email Notifications" />
<SettingRow label="SMS Alerts" />
</View>
<SectionTitle title="Privacy" />
<View style={styles.section}>
<SettingRow label="Data Sharing" />
<SettingRow label="Analytics" />
<SettingRow label="Delete Account" />
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
paddingVertical: 20,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#666',
textTransform: 'uppercase',
marginHorizontal: 16,
marginTop: 20,
marginBottom: 8,
},
section: {
backgroundColor: 'white',
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: '#e0e0e0',
},
settingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
settingLabel: {
fontSize: 16,
color: '#333',
},
chevron: {
fontSize: 20,
color: '#ccc',
},
});
Exercise 2: Horizontal Category Picker
Goal: Create a horizontal scrolling category picker.
Requirements:
- Horizontal ScrollView that doesn't show the scroll indicator
- Array of category buttons (pills)
- One category is "selected" with different styling
- Tapping a category selects it
- Padding on the sides so first/last items aren't flush with edges
💡 Hint
Use useState to track the selected category. Use Pressable for the pills. Apply different styles based on whether the item is selected.
✅ Solution
import { useState } from 'react';
import { ScrollView, Pressable, Text, StyleSheet } from 'react-native';
const CATEGORIES = [
'All', 'Technology', 'Design', 'Business',
'Science', 'Health', 'Sports', 'Entertainment'
];
export default function CategoryPicker() {
const [selected, setSelected] = useState('All');
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
>
{CATEGORIES.map((category) => (
<Pressable
key={category}
onPress={() => setSelected(category)}
style={[
styles.pill,
selected === category && styles.pillSelected,
]}
>
<Text
style={[
styles.pillText,
selected === category && styles.pillTextSelected,
]}
>
{category}
</Text>
</Pressable>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 12,
gap: 8,
},
pill: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#f0f0f0',
borderWidth: 1,
borderColor: '#e0e0e0',
},
pillSelected: {
backgroundColor: '#2196F3',
borderColor: '#2196F3',
},
pillText: {
fontSize: 14,
fontWeight: '500',
color: '#666',
},
pillTextSelected: {
color: 'white',
},
});
Exercise 3: Pull-to-Refresh Feed
Goal: Create a simple feed with pull-to-refresh functionality.
Requirements:
- ScrollView with RefreshControl
- Display a list of "posts" (simple cards with title and timestamp)
- Pulling down triggers a refresh with 1.5 second delay
- After refresh, add a new post at the top
- Show the refresh spinner while loading
💡 Hint
Use useState for both the posts array and the refreshing state. Use useCallback for the refresh handler. Generate a timestamp with new Date().toLocaleTimeString().
✅ Solution
import { useState, useCallback } from 'react';
import {
ScrollView, RefreshControl, View, Text, StyleSheet
} from 'react-native';
interface Post {
id: number;
title: string;
timestamp: string;
}
const initialPosts: Post[] = [
{ id: 1, title: 'Welcome to the feed!', timestamp: '10:00 AM' },
{ id: 2, title: 'This is a sample post', timestamp: '9:45 AM' },
{ id: 3, title: 'Pull down to refresh', timestamp: '9:30 AM' },
];
export default function RefreshableFeed() {
const [posts, setPosts] = useState<Post[]>(initialPosts);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Add new post at the top
const newPost: Post = {
id: Date.now(),
title: `New post #${posts.length + 1}`,
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
}),
};
setPosts(prev => [newPost, ...prev]);
setRefreshing(false);
}, [posts.length]);
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#2196F3"
colors={['#2196F3']}
/>
}
>
{posts.map((post) => (
<View key={post.id} style={styles.card}>
<Text style={styles.title}>{post.title}</Text>
<Text style={styles.timestamp}>{post.timestamp}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 16,
},
card: {
backgroundColor: 'white',
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 4,
},
timestamp: {
fontSize: 12,
color: '#999',
},
});
Challenge: Scroll-to-Section Navigation
🏆 Bonus Challenge
Goal: Create a scrollable page with section navigation that scrolls to each section when tapped.
Features:
- Horizontal navigation bar at the top with section names
- Vertical ScrollView with multiple content sections
- Tapping a nav item scrolls to that section smoothly
- Use
scrollToand measure section positions withonLayout
✅ Solution
import { useRef, useState } from 'react';
import {
ScrollView, View, Text, Pressable, StyleSheet
} from 'react-native';
const SECTIONS = ['Overview', 'Features', 'Pricing', 'FAQ'];
export default function ScrollToSectionDemo() {
const scrollViewRef = useRef<ScrollView>(null);
const [sectionPositions, setSectionPositions] = useState<number[]>([]);
const handleSectionLayout = (index: number, y: number) => {
setSectionPositions(prev => {
const newPositions = [...prev];
newPositions[index] = y;
return newPositions;
});
};
const scrollToSection = (index: number) => {
const y = sectionPositions[index];
if (y !== undefined) {
scrollViewRef.current?.scrollTo({ y, animated: true });
}
};
return (
<View style={styles.container}>
{/* Navigation */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.nav}
contentContainerStyle={styles.navContent}
>
{SECTIONS.map((section, index) => (
<Pressable
key={section}
onPress={() => scrollToSection(index)}
style={styles.navItem}
>
<Text style={styles.navText}>{section}</Text>
</Pressable>
))}
</ScrollView>
{/* Content */}
<ScrollView
ref={scrollViewRef}
style={styles.content}
>
{SECTIONS.map((section, index) => (
<View
key={section}
onLayout={(e) => handleSectionLayout(index, e.nativeEvent.layout.y)}
style={styles.section}
>
<Text style={styles.sectionTitle}>{section}</Text>
<Text style={styles.sectionContent}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
{'\n\n'}
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
</Text>
</View>
))}
<View style={{ height: 100 }} />
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
nav: {
maxHeight: 50,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
navContent: {
paddingHorizontal: 16,
alignItems: 'center',
gap: 8,
},
navItem: {
paddingVertical: 12,
paddingHorizontal: 16,
},
navText: {
fontSize: 14,
fontWeight: '600',
color: '#2196F3',
},
content: {
flex: 1,
},
section: {
padding: 20,
minHeight: 300,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
sectionTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 16,
},
sectionContent: {
fontSize: 16,
lineHeight: 24,
color: '#666',
},
});
Summary
🎉 Key Takeaways
- View doesn't scroll — use ScrollView for scrollable content
- style vs contentContainerStyle — style affects the frame; contentContainerStyle affects content
- horizontal prop — enables horizontal scrolling
- pagingEnabled — snaps to page boundaries (great for carousels)
- onScroll + scrollEventThrottle — track scroll position (16ms for smooth animations)
- RefreshControl — enables pull-to-refresh pattern
- Keyboard props — keyboardDismissMode and keyboardShouldPersistTaps for forms
- Don't use for long lists — ScrollView renders all children at once; use FlatList instead
Quick Reference
import { ScrollView, RefreshControl } from 'react-native';
// Basic vertical scroll
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 16 }}
>
// Horizontal with paging
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
>
// With pull-to-refresh
<ScrollView
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />
}
>
// For forms
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
>
// Scroll programmatically
const ref = useRef<ScrollView>(null);
ref.current?.scrollTo({ y: 0, animated: true });
ref.current?.scrollToEnd({ animated: true });
🚀 What's Next?
Now that you understand scrolling, we'll explore SafeAreaView — how to respect device boundaries like notches, home indicators, and status bars for proper content positioning.
📜 Scrolling Mastered!
You now know how to create scrollable interfaces, handle pull-to-refresh, work with keyboards, and when to choose ScrollView vs FlatList. Smooth scrolling ahead!