Skip to main content

Transition.ScrollView

Use Transition.ScrollView instead of the standard ScrollView when you need gesture coordination:
import Transition from "react-native-screen-transitions";
import { View, Text } from "react-native";

function DetailScreen() {
  return (
    <Transition.ScrollView style={{ flex: 1 }} bounces={false}>
      <View style={{ padding: 20 }}>
        <Text>Scrollable content goes here</Text>
      </View>
    </Transition.ScrollView>
  );
}
When gestures are enabled, the scroll view automatically coordinates with the dismiss gesture.

Gesture Coordination Rules

The gesture activates based on scroll position and gesture direction:
DirectionActivates When
verticalScroll is at top
vertical-invertedScroll is at bottom
horizontalScroll is at left or right edge
For example, with vertical gesture direction:
  1. User scrolls down → scroll content moves
  2. Scroll reaches top → gesture can now activate
  3. User continues dragging down → gesture takes over and dismisses
options={{
  gestureEnabled: true,
  gestureDirection: "vertical",  // Activates at top
}}

Bottom Sheet with ScrollView

A common pattern is a bottom sheet with scrollable content:
import Transition from "react-native-screen-transitions";
import { View, Text, FlatList } from "react-native";
import { interpolate } from "react-native-reanimated";

function BottomSheetScreen() {
  const items = Array.from({ length: 20 }, (_, i) => ({
    id: String(i),
    title: `Item ${i + 1}`,
  }));

  return (
    <Transition.ScrollView
      style={{ flex: 1, backgroundColor: "#fff" }}
      scrollEnabled={true}
      bounces={false}
    >
      <View style={{ paddingTop: 20, paddingHorizontal: 20 }}>
        <Text style={{ fontSize: 20, fontWeight: "bold", marginBottom: 20 }}>
          Options
        </Text>
        {items.map((item) => (
          <View
            key={item.id}
            style={{
              paddingVertical: 12,
              borderBottomWidth: 1,
              borderBottomColor: "#eee",
            }}
          >
            <Text>{item.title}</Text>
          </View>
        ))}
      </View>
    </Transition.ScrollView>
  );
}

export const options = {
  snapPoints: [0.5, 1],
  initialSnapIndex: 0,
  screenStyleInterpolator: ({ progress }) => {
    "worklet";
    return {
      contentStyle: {
        borderTopLeftRadius: 16,
        borderTopRightRadius: 16,
        transform: [
          {
            translateY: interpolate(progress, [0, 1, 2], [600, 0, 600]),
          },
        ],
      },
      backdropStyle: {
        opacity: interpolate(progress, [0, 1, 2], [0, 0.3, 0]),
      },
    };
  },
  gestureEnabled: true,
  gestureDirection: "vertical",
  backdropBehavior: "collapse",
};

Transition.FlatList

Use Transition.FlatList for better performance with long lists:
import Transition from "react-native-screen-transitions";

function ListScreen() {
  const data = Array.from({ length: 100 }, (_, i) => ({
    id: String(i),
    title: `Item ${i + 1}`,
  }));

  return (
    <Transition.FlatList
      data={data}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: "#eee" }}>
          <Text>{item.title}</Text>
        </View>
      )}
      keyExtractor={(item) => item.id}
      bounces={false}
    />
  );
}

expandViaScrollView Behavior

When expandViaScrollView is true (default with snap points):
options={{
  snapPoints: [0.5, 1],
  expandViaScrollView: true,  // Swipe up expands
}}
The bottom sheet expands when the user swipes up from the scrollable boundary. If the scroll is at the top and the user swipes up, the sheet expands to the next snap point. When expandViaScrollView is false:
options={{
  snapPoints: [0.5, 1],
  expandViaScrollView: false,  // Swipe only in deadspace
}}
The sheet only expands when swiping in the area above the scroll content (deadspace).

Header + Scrollable Content

Create a sheet with a fixed header and scrollable body:
import Transition from "react-native-screen-transitions";
import { View, Text } from "react-native";

function SheetWithHeader() {
  return (
    <View style={{ flex: 1 }}>
      {/* Fixed header */}
      <View style={{ padding: 20, borderBottomWidth: 1, borderBottomColor: "#eee" }}>
        <Text style={{ fontSize: 18, fontWeight: "bold" }}>Sheet Title</Text>
      </View>

      {/* Scrollable content */}
      <Transition.ScrollView style={{ flex: 1 }} bounces={false}>
        <View style={{ padding: 20 }}>
          {Array.from({ length: 20 }).map((_, i) => (
            <Text key={i} style={{ marginBottom: 12 }}>
              Content item {i + 1}
            </Text>
          ))}
        </View>
      </Transition.ScrollView>
    </View>
  );
}

Preventing Accidental Dismissal

When using swipe gestures with scrollable content, users may accidentally dismiss when trying to scroll:
  1. Disable gesture on vertical scroll sheets — Let scrolling take priority:
    options={{
      gestureEnabled: true,
      gestureDirection: "vertical",
      gestureActivationArea: "edge",  // Only swipe at edges
    }}
    
  2. Use snap points instead — Snap points let users collapse instead of dismiss:
    options={{
      snapPoints: [0, 0.5, 1],  // Three positions
      backdropBehavior: "collapse",  // Swipe collapses
    }}
    
  3. Require high velocity — Ignore slow drags:
    options={{
      gestureVelocityImpact: 0.5,  // Require more velocity
    }}
    

Performance Tips

  1. Use FlatList for long lists — Better than ScrollView for performance
  2. Remove bounces — Set bounces={false} for smoother gesture transitions
  3. Limit viewport — Use FlatList’s scrollEventThrottle to reduce re-renders
  4. Avoid nested scrolls — Only one ScrollView/FlatList per screen
<Transition.FlatList
  data={data}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  bounces={false}
  scrollEventThrottle={16}
  maxToRenderPerBatch={10}
/>

Example: Complete Bottom Sheet

import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
import { View, Text, TouchableOpacity } from "react-native";
import { interpolate } from "react-native-reanimated";

const Stack = createBlankStackNavigator();

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <TouchableOpacity onPress={() => navigation.navigate("Sheet")}>
        <Text>Open Sheet</Text>
      </TouchableOpacity>
    </View>
  );
}

function SheetScreen() {
  return (
    <Transition.ScrollView
      style={{ flex: 1, backgroundColor: "#fff" }}
      bounces={false}
    >
      <View style={{ padding: 20 }}>
        <Text style={{ fontSize: 20, fontWeight: "bold", marginBottom: 16 }}>
          Sheet Contents
        </Text>
        {Array.from({ length: 30 }).map((_, i) => (
          <View key={i} style={{ marginBottom: 16 }}>
            <Text>Item {i + 1}</Text>
          </View>
        ))}
      </View>
    </Transition.ScrollView>
  );
}

export default function App() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="Sheet"
        component={SheetScreen}
        options={{
          snapPoints: [0.5, 1],
          initialSnapIndex: 0,
          screenStyleInterpolator: ({ progress }) => {
            "worklet";
            return {
              contentStyle: {
                borderTopLeftRadius: 16,
                borderTopRightRadius: 16,
                transform: [
                  {
                    translateY: interpolate(progress, [0, 1, 2], [600, 0, 600]),
                  },
                ],
              },
              backdropStyle: {
                opacity: interpolate(progress, [0, 1, 2], [0, 0.3, 0]),
              },
            };
          },
          gestureEnabled: true,
          gestureDirection: "vertical",
          backdropBehavior: "collapse",
          expandViaScrollView: true,
        }}
      />
    </Stack.Navigator>
  );
}

Next Steps