Skip to main content

Overview

ScrollViews inside sheets create complex gesture scenarios. The library provides automatic coordination to ensure smooth interaction between scroll and navigation gestures.

Basic ScrollView in Sheet

Use Transition.ScrollView for built-in gesture coordination:
import { Transition } from 'react-native-screen-transitions';

export default function SheetContent() {
  return (
    <Transition.ScrollView>
      <View style={{ padding: 20 }}>
        <Text style={{ fontSize: 24, fontWeight: 'bold' }}>Sheet Title</Text>
        {/* Long content here */}
      </View>
    </Transition.ScrollView>
  );
}
Transition.ScrollView automatically handles gesture passing at scroll boundaries.

Scroll Boundary Behavior

At the top of the ScrollView (when scrollY = 0), the gesture can transfer to the sheet:
<Transition.Modal
  snapPoints={[0.4, 1]}
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <Transition.ScrollView>
    <LongContent />
  </Transition.ScrollView>
</Transition.Modal>
Behavior:
  • Scrolling down (anywhere): ScrollView scrolls normally
  • At top, dragging up: Sheet expands to next snap point
  • Below last snap point: Sheet snaps back up
  • Dragging down: Scrolls or collapses sheet

Gesture Behavior Modes

Expand and Collapse (Default)

Users can expand the sheet by dragging up at the scroll boundary:
<Transition.Modal
  snapPoints={[0.5, 1]}
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <Transition.ScrollView>
    {/* Content */}
  </Transition.ScrollView>
</Transition.Modal>
Perfect for sheets where users need to both scroll and resize.

Collapse Only

Users cannot expand via scroll—must use dead space:
<Transition.Modal
  snapPoints={[0.5, 1]}
  sheetScrollGestureBehavior="collapse-only"
  gestureEnabled
>
  <Transition.ScrollView>
    {/* Content */}
  </Transition.ScrollView>
</Transition.Modal>
The ScrollView has priority at all times. To expand, users must drag from a header or empty space.

Dead Space for Expansion

Create header space outside the ScrollView for manual sheet expansion:
export default function SheetContent() {
  return (
    <View style={{ flex: 1 }}>
      {/* Dead space for dragging to expand */}
      <View style={{ height: 20, backgroundColor: 'transparent' }} />

      <Text style={{ fontSize: 18, fontWeight: 'bold', paddingHorizontal: 20 }}>
        Sheet Header
      </Text>

      <Transition.ScrollView>
        <LongContent />
      </Transition.ScrollView>
    </View>
  );
}
Users can drag from the header area to expand the sheet.

FlatList Coordination

Use Transition.FlatList for lists with built-in coordination:
<Transition.Modal
  snapPoints={["auto", 1]}
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <Transition.FlatList
    data={items}
    renderItem={({ item }) => <ItemComponent {...item} />}
    keyExtractor={(item) => item.id}
    scrollEventThrottle={16}
  />
</Transition.Modal>
The FlatList automatically passes gestures to the sheet at its top boundary.

SectionList Coordination

SectionLists also support gesture coordination:
<Transition.Modal snapPoints={[0.5, 1]} gestureEnabled>
  <Transition.SectionList
    sections={sections}
    keyExtractor={(item, index) => item + index}
    renderItem={({ item }) => <ItemComponent item={item} />}
    renderSectionHeader={({ section: { title } }) => (
      <Text style={{ fontSize: 16, fontWeight: 'bold' }}>{title}</Text>
    )}
  />
</Transition.Modal>

Nested ScrollViews

Nested scrollable content requires explicit coordination:
<Transition.Modal
  snapPoints={[0.5, 1]}
  sheetScrollGestureBehavior="collapse-only"
  gestureEnabled
>
  <Transition.ScrollView>
    <Transition.FlatList
      data={items}
      renderItem={({ item }) => <ItemComponent {...item} />}
      scrollEnabled={false}  // Disable inner scroll
    />
  </Transition.ScrollView>
</Transition.Modal>
Set scrollEnabled={false} on inner scrollables to prevent conflicts.

Custom Scroll Coordination

For non-Transition ScrollViews, coordinate manually with useSharedValue:
import Reanimated, {
  useSharedValue,
  useAnimatedScrollHandler,
} from 'react-native-reanimated';
import { useScreenGesture } from 'react-native-screen-transitions';

export default function CustomScrollSheet() {
  const scrollY = useSharedValue(0);

  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
    },
  });

  useScreenGesture("self", {
    gestureEnabled: true,
    gestureDrivesProgress: scrollY.value > 0,  // Only at top
  });

  return (
    <Reanimated.ScrollView onScroll={scrollHandler}>
      {/* Content */}
    </Reanimated.ScrollView>
  );
}

Handling Velocity at Boundary

Control snap point selection when releasing with velocity:
<Transition.Modal
  snapPoints={[0.4, 0.7, 1]}
  snapVelocityImpact={0.15}  // Low impact = snap to nearest
  gestureReleaseVelocityScale={1.2}
  gestureReleaseVelocityMax={2000}
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <Transition.ScrollView>
    {/* Content */}
  </Transition.ScrollView>
</Transition.Modal>

ScrollView with Header

Add a fixed header above the scrollable content:
export default function SheetWithHeader() {
  return (
    <View style={{ flex: 1 }}>
      {/* Fixed header */}
      <View style={{
        paddingVertical: 16,
        paddingHorizontal: 20,
        borderBottomWidth: 1,
        borderBottomColor: '#e0e0e0',
      }}>
        <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Sheet Title</Text>
      </View>

      {/* Scrollable content */}
      <Transition.ScrollView>
        <LongContent />
      </Transition.ScrollView>
    </View>
  );
}

Performance Optimization

Scroll Throttling

Limit scroll event frequency for better performance:
<Transition.ScrollView
  scrollEventThrottle={16}  // ~60fps (1000/60 ≈ 16ms)
>
  {/* Content */}
</Transition.ScrollView>
Lower values = more frequent updates but more CPU usage.

Remove Scroll Indicators

Hide scrollbar for cleaner appearance:
<Transition.ScrollView
  showsVerticalScrollIndicator={false}
  showsHorizontalScrollIndicator={false}
>
  {/* Content */}
</Transition.ScrollView>

Memoize List Items

For large lists, memoize item components:
const ItemComponent = React.memo(({ item }) => (
  <View style={{ padding: 10 }}>
    <Text>{item.title}</Text>
  </View>
));

<Transition.FlatList
  data={largeList}
  renderItem={({ item }) => <ItemComponent item={item} />}
  keyExtractor={(item) => item.id}
  maxToRenderPerBatch={10}
  updateCellsBatchingPeriod={50}
/>

Common Patterns

Expandable Sheet with Scroll

Sheet expands on demand, then allows scrolling:
<Transition.Modal
  name="ExpandableSheet"
  snapPoints={[0.3, 0.7, 1]}
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <View style={{ flex: 1 }}>
    {/* Draggable header */}
    <View style={{ height: 40, justifyContent: 'center', alignItems: 'center' }}>
      <View style={{ width: 40, height: 4, backgroundColor: '#ccc', borderRadius: 2 }} />
    </View>

    {/* Scrollable content */}
    <Transition.ScrollView>
      <LongContent />
    </Transition.ScrollView>
  </View>
</Transition.Modal>

Static Header, Scrollable Body

Fixed header with content below:
<View style={{ flex: 1 }}>
  <StaticHeader />
  <Transition.ScrollView>
    <ScrollableContent />
  </Transition.ScrollView>
</View>

Pull-to-Refresh

Combine scroll with custom refresh logic:
import { RefreshControl } from 'react-native';

export default function RefreshableSheet() {
  const [refreshing, setRefreshing] = useState(false);

  const onRefresh = () => {
    setRefreshing(true);
    // Fetch data
    setTimeout(() => setRefreshing(false), 1000);
  };

  return (
    <Transition.ScrollView
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
    >
      {/* Content */}
    </Transition.ScrollView>
  );
}

Troubleshooting

ScrollView Not Scrolling

Ensure ScrollView is not inside a View with flex: 1:
// ❌ Wrong - ScrollView won't measure properly
<View style={{ flex: 1 }}>
  <Transition.ScrollView>
    {/* Content */}
  </Transition.ScrollView>
</View>

// ✅ Correct
<Transition.ScrollView style={{ flex: 1 }}>
  {/* Content */}
</Transition.ScrollView>

Gesture Not Transferring at Boundary

Check:
  1. ScrollView is at top (scrollY = 0)
  2. sheetScrollGestureBehavior is not “collapse-only”
  3. gestureEnabled is true

FlatList Items Not Rendering

Verify data and key extractor:
<Transition.FlatList
  data={items}
  renderItem={({ item }) => <ItemComponent {...item} />}
  keyExtractor={(item) => item.id}  // Must be unique and stable
/>

Best Practices

  1. Use Transition.ScrollView for automatic coordination
  2. Set scrollEventThrottle to reduce CPU usage
  3. Memoize list items for large lists
  4. Test scroll + gesture together before shipping
  5. Provide visual feedback for scroll state
  6. Throttle expensive operations in scroll handler

Next Steps