ScrollView
ScrollView renders all children at once and lets the user scroll through them. Use it for SHORT content (forms, settings); for long lists, switch to FlatList.
Scrolling, keyboards, sticky headers
EXAMPLE
import { useRef, useState } from 'react';
import {
ScrollView,
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
RefreshControl,
SafeAreaView,
StatusBar,
} from 'react-native';
// 1) Basic vertical scroll
function Settings() {
return (
<ScrollView contentContainerStyle={{ padding: 16 }}>
<Section title="Profile">…</Section>
<Section title="Notifications">…</Section>
<Section title="Privacy">…</Section>
<Section title="Account">…</Section>
</ScrollView>
);
}
// 2) Horizontal scroll (stories, image carousel)
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{items.map(item => (
<Card key={item.id} item={item} />
))}
</ScrollView>
// 3) Paging (snap-to-page) — full-screen swipe
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
>
{pages.map(page => (
<View key={page.id} style={{ width: SCREEN_W }}>
…
</View>
))}
</ScrollView>
// 4) Sticky headers — stay visible while scrolling
<ScrollView stickyHeaderIndices={[0]}>
<View style={styles.sticky}>
<Text style={styles.stickyText}>Top header</Text>
</View>
{sections.map(s => <SectionView key={s.id} section={s} />)}
</ScrollView>
// 5) Pull-to-refresh
function Feed() {
const [refreshing, setRefreshing] = useState(false);
async function onRefresh() {
setRefreshing(true);
await refetch();
setRefreshing(false);
}
return (
<ScrollView
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{items.map(item => <Item key={item.id} item={item} />)}
</ScrollView>
);
}
// 6) Detect scroll position
function WithHeaderShadow() {
const [shadow, setShadow] = useState(false);
return (
<View>
<View style={[styles.header, shadow && styles.headerShadow]}>
<Text>App</Text>
</View>
<ScrollView
onScroll={e => setShadow(e.nativeEvent.contentOffset.y > 4)}
scrollEventThrottle={16}
>
{/* … */}
</ScrollView>
</View>
);
}
// 7) Keyboard handling — KeyboardAvoidingView wrap
function Form() {
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
keyboardShouldPersistTaps="handled"
contentContainerStyle={{ padding: 16 }}
>
<TextField label="Name" />
<TextField label="Email" />
<TextField label="Comment" multiline />
<Button title="Submit" onPress={submit} />
</ScrollView>
</KeyboardAvoidingView>
);
}
// keyboardShouldPersistTaps:
// 'always' — taps pass through; keyboard never dismisses on tap
// 'handled' — taps dismiss keyboard unless the child has its own handler
// 'never' — taps always dismiss the keyboard (default)
// 8) Scroll programmatically
const ref = useRef(null);
function scrollToTop() { ref.current.scrollTo({ y: 0, animated: true }); }
function scrollToBottom() { ref.current.scrollToEnd({ animated: true }); }
function scrollToY(y) { ref.current.scrollTo({ y, animated: true }); }
<ScrollView ref={ref}>…</ScrollView>
// 9) Center vertically (small content, big screen)
<ScrollView contentContainerStyle={{ flexGrow: 1, justifyContent: 'center', padding: 16 }}>
{/* … */}
</ScrollView>
// 10) Scrolling images / video — bounces feel right
<ScrollView
bounces={true} // iOS bounce effect
overScrollMode="never" // Android — disable glow
showsVerticalScrollIndicator={false}
>
{/* … */}
</ScrollView>
// 11) Combine with SafeAreaView (notch + home indicator)
<SafeAreaView style={{ flex: 1 }}>
<ScrollView>
{/* … */}
</ScrollView>
</SafeAreaView>
// 12) Common bugs
// • Wrapping a FlatList inside a ScrollView — virtualisation breaks, performance dies
// • Forgetting contentContainerStyle for padding (style is the OUTER, contentContainerStyle is the INNER)
// • Long lists in ScrollView → mount EVERY child, slow first paint + memory
// • Form inputs hidden by keyboard → wrap with KeyboardAvoidingView
// • Sticky headers not working → check stickyHeaderIndices is correct
// • Horizontal ScrollView with width-less children → no scrolling (give children explicit width)
// 13) Style vs contentContainerStyle
<ScrollView
style={{ flex: 1, backgroundColor: '#f8fafc' }} // OUTER container
contentContainerStyle={{ padding: 16, gap: 12 }} // INNER content area
>
{/* … */}
</ScrollView>
// 14) Animated header — collapse on scroll
import Animated, { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, interpolate } from 'react-native-reanimated';
function CollapsingHeader() {
const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler(e => { scrollY.value = e.contentOffset.y; });
const headerStyle = useAnimatedStyle(() => ({
height: interpolate(scrollY.value, [0, 100], [120, 56], 'clamp'),
opacity: interpolate(scrollY.value, [0, 100], [1, 0.7], 'clamp'),
}));
return (
<View style={{ flex: 1 }}>
<Animated.View style={[styles.header, headerStyle]}>
<Text style={styles.headerText}>App</Text>
</Animated.View>
<Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
{/* … */}
</Animated.ScrollView>
</View>
);
}
// 15) When to use what
// ScrollView : 1-30 children, short content, settings, forms, single-screen scrolling
// FlatList : 20+ items, lists from data arrays — virtualised
// SectionList : grouped data
// FlashList : @shopify/flash-list, fastest virtualised list
// CustomScrollView with SliverList : Flutter equivalent
const styles = StyleSheet.create({
header: { height: 56, paddingHorizontal: 16, justifyContent: 'center', backgroundColor: 'white' },
headerShadow: { shadowOpacity: 0.1, shadowOffset: { width: 0, height: 2 }, shadowRadius: 4, elevation: 2 },
sticky: { padding: 12, backgroundColor: '#0ea5e9' },
stickyText: { color: 'white', fontWeight: '600' },
});
Why it matters
Use ScrollView for short, fixed content (forms, settings). For long lists, switch to FlatList — it virtualises off-screen rows. Wrapping a FlatList inside a ScrollView breaks virtualisation entirely.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { ScrollView, Text } from 'react-native';
<ScrollView contentContainerStyle={{ padding: 16 }}>
{items.map(i => <Text key={i.id}>{i.label}</Text>)}
</ScrollView>
Try it Yourself »
Discussion
Loading…