Skip to main content

Overview

Snap points allow screens to rest at multiple heights, creating bottom sheets, top sheets, or side sheets. The API works with any gesture direction, making it versatile for various UI patterns.

Basic Configuration

Bottom Sheet

The most common use case - a sheet that slides up from the bottom:
<Stack.Screen
  name="Sheet"
  options={{
    gestureEnabled: true,
    gestureDirection: "vertical",
    snapPoints: [0.5, 1],         // 50% and 100% of screen height
    initialSnapIndex: 0,          // Start at 50%
    backdropBehavior: "dismiss",  // Tap backdrop to dismiss
    ...Transition.Presets.SlideFromBottom(),
  }}
/>

Side Sheet

Create a drawer-style sheet from the side:
<Stack.Screen
  name="SidePanel"
  options={{
    gestureEnabled: true,
    gestureDirection: "horizontal",
    snapPoints: [0.3, 0.7, 1],    // 30%, 70%, 100% of screen width
    initialSnapIndex: 1,          // Start at 70%
    // Add a horizontal screenStyleInterpolator for drawer-style motion
  }}
/>

Top Sheet

A sheet that slides down from the top:
<Stack.Screen
  name="TopSheet"
  options={{
    gestureEnabled: true,
    gestureDirection: "vertical-inverted",
    snapPoints: [0.4, 0.8],       // 40% and 80% of screen height
    initialSnapIndex: 0,
    ...Transition.Presets.SlideFromTop(),
  }}
/>

Configuration Options

snapPoints
number[]
default:"[1.0]"
Array of fractions (0-1) where the sheet can rest. Values represent percentages of screen height (for vertical) or width (for horizontal).Example: [0.5, 1] creates two stops at 50% and 100%.
initialSnapIndex
number
default:0
Index of the snap point where the screen initially appears. Use 0 for the first snap point, 1 for the second, etc.
gestureSnapLocked
boolean
default:false
Locks gesture-based snap movement to the current snap point. When enabled, users cannot swipe between snap points (but programmatic snapTo() still works). If dismiss gestures are allowed (gestureEnabled !== false), swipe-to-dismiss still functions.
backdropBehavior
string
default:"'block'"
Controls backdrop touch handling:
  • "block": Backdrop catches all touches
  • "passthrough": Touches pass through to content behind
  • "dismiss": Tapping backdrop dismisses the screen
  • "collapse": Tapping backdrop collapses to next lower snap point, then dismisses at minimum
backdropComponent
React.FC
Custom backdrop component. When provided, replaces the default backdrop entirely (including default tap behavior). You’re responsible for implementing dismiss/collapse actions. backdropBehavior still controls container-level pointer events.

Programmatic Control

Control snap points from anywhere in your component tree:
import { snapTo } from "react-native-screen-transitions";

function BottomSheet() {
  // Expand to full height (index 1)
  const expand = () => snapTo(1);

  // Collapse to half height (index 0)
  const collapse = () => snapTo(0);

  return (
    <View>
      <Button title="Expand" onPress={expand} />
      <Button title="Collapse" onPress={collapse} />
    </View>
  );
}
snapTo() works even when gestureSnapLocked is true. The lock only affects gesture-driven snapping.

Snap Index in Animations

The animated snapIndex is available in your screen interpolator:
screenStyleInterpolator: ({ snapIndex }) => {
  "worklet";
  
  // snapIndex interpolates between snap point indices
  // e.g., 0.5 means halfway between snap point 0 and 1
  return {
    contentStyle: {
      opacity: interpolate(snapIndex, [0, 1], [0.5, 1]),
    },
  };
}
snapIndex
number
Animated index of the current snap point:
  • Returns -1 if no snap points are defined
  • Returns 0 when at or below first snap point
  • Returns fractional values between snap points (e.g., 1.5 = halfway between snap 1 and 2)
  • Returns length-1 when at or above last snap point

Example: Fade Header Based on Snap Position

screenStyleInterpolator: ({ snapIndex }) => {
  "worklet";
  
  const headerOpacity = interpolate(
    snapIndex,
    [0, 1],
    [0, 1],
    "clamp"
  );

  return {
    "sheet-header": {
      opacity: headerOpacity,
    },
  };
}

// In your component:
<Transition.View styleId="sheet-header">
  <Text>Header</Text>
</Transition.View>

ScrollView Behavior

When using Transition.ScrollView inside a snap-enabled sheet:
Apple Maps StyleAt scroll boundary:
  • Swipe up → expands to next snap point
  • Swipe down → collapses to previous snap point (or dismisses at min)
When scrolled into content:
  • Normal scroll behavior
options={{
  snapPoints: [0.5, 1],
  expandViaScrollView: true,
}}
expandViaScrollView
boolean
default:true
Allow expansion from ScrollView at boundary. When false, expand only works via deadspace (non-scrollable areas).

Custom Backdrop

Create a custom backdrop with full control over appearance and behavior:
import { router } from "expo-router";
import { Pressable } from "react-native";
import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated";
import { useScreenAnimation } from "react-native-screen-transitions";

function SheetBackdrop() {
  const animation = useScreenAnimation();

  const style = useAnimatedStyle(() => ({
    opacity: interpolate(
      animation.value.current.progress,
      [0, 1],
      [0, 0.4]
    ),
    backgroundColor: "#000",
  }));

  return (
    <Pressable style={{ flex: 1 }} onPress={() => router.back()}>
      <Animated.View style={[{ flex: 1 }, style]} />
    </Pressable>
  );
}

<Stack.Screen
  name="Sheet"
  options={{
    snapPoints: [0.5, 1],
    backdropBehavior: "dismiss",
    backdropComponent: SheetBackdrop,
  }}
/>
When using backdropComponent, you’re responsible for implementing tap-to-dismiss or collapse behavior. The default backdrop press handling is replaced entirely.

Animation Specs

Customize snap animations separately from enter/exit transitions:
options={{
  snapPoints: [0.5, 1],
  transitionSpec: {
    open: { stiffness: 1000, damping: 500, mass: 3 },   // Screen enters
    close: { stiffness: 1000, damping: 500, mass: 3 },  // Screen exits
    expand: { stiffness: 300, damping: 30 },            // Snap up
    collapse: { stiffness: 300, damping: 30 },          // Snap down
  },
}}
transitionSpec.expand
AnimationConfig
Animation configuration when expanding to a higher snap point.Uses lower intensity than open because snap movements are typically smaller than full enter/exit transitions.

Gesture Lock Behavior

Control whether users can swipe between snap points:
options={{
  snapPoints: [0.3, 0.7, 1],
  gestureSnapLocked: true,  // Lock gesture-based snapping
  gestureEnabled: true,     // Still allow swipe-to-dismiss
}}
Default Behavior
  • Users can swipe between all snap points
  • Swipe-to-dismiss works (if gestureEnabled: true)
  • Programmatic snapTo() works

Velocity Impact

Control how swipe velocity affects snap targeting:
options={{
  snapPoints: [0.5, 1],
  snapVelocityImpact: 0.1,  // Lower = more deliberate (iOS-like)
}}
snapVelocityImpact
number
How much velocity affects snap point targeting:
  • Lower values (0.05-0.1): More deliberate, iOS-like feel
  • Higher values (0.2-0.5): More responsive to quick flicks
  • Default of 0.1 provides a balanced feel

Examples

Multi-Stop Bottom Sheet

<Stack.Screen
  name="MultiStop"
  options={{
    gestureEnabled: true,
    gestureDirection: "vertical",
    snapPoints: [0.3, 0.6, 0.9],  // Three stops
    initialSnapIndex: 1,           // Start at middle (60%)
    backdropBehavior: "collapse",  // Tap to collapse
    screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
      "worklet";
      return {
        contentStyle: {
          transform: [{
            translateY: interpolate(
              progress,
              [0, 1],
              [screen.height, 0]
            ),
          }],
        },
        backdropStyle: {
          opacity: interpolate(progress, [0, 1], [0, 0.5]),
          backgroundColor: "#000",
        },
      };
    },
    transitionSpec: {
      open: { stiffness: 1000, damping: 500, mass: 3 },
      close: { stiffness: 1000, damping: 500, mass: 3 },
      expand: { stiffness: 400, damping: 40 },
      collapse: { stiffness: 400, damping: 40 },
    },
  }}
/>

Horizontal Drawer

<Stack.Screen
  name="Drawer"
  options={{
    gestureEnabled: true,
    gestureDirection: "horizontal",
    snapPoints: [0.7, 1],          // 70% and full width
    initialSnapIndex: 0,
    screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
      "worklet";
      return {
        contentStyle: {
          transform: [{
            translateX: interpolate(
              progress,
              [0, 1],
              [-screen.width, 0]
            ),
          }],
        },
      };
    },
  }}
/>