Skip to main content

📜 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 vs ScrollView View (No Scroll) Item 1 ✓ Item 2 ✓ Item 3 ✓ Item 4 ✗ (clipped) Content cut off! ScrollView Item 1 ✓ Item 2 ✓ Item 3 ✓ Item 4 ✓ Scroll to see all ↕

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 vs contentContainerStyle style backgroundColor, flex contentContainerStyle padding, gap, alignItems ScrollView frame Content wrapper

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 Event Values contentSize.height Viewport layoutMeasurement.height contentOffset.y event.nativeEvent: • contentOffset.y = 50 • contentSize.height = 240 • layoutMeasurement = 100

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 refreshing to false when done, even on error
  • Use useCallback to 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 scrollTo and measure section positions with onLayout
✅ 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!