Smooth Gesture-Based Modals in React Native – Sheet Transitions

Description:

React Native Sheet Transitions is a modal library that recreates gesture-based, iOS-inspired sheet animations for React Native, Expo Go, and web platforms.

The library handles scale transformations, border radius synchronization, and drag-based dismissal with haptic feedback. You can customize animation timing, drag directions, and background effects through a declarative API.

It integrates with React Native Reanimated and React Native Gesture Handler to deliver frame-accurate animations. You control sheet behavior through provider configuration and component props without managing animation state manually.

Features

  • 🔄 Scale Transitions: Background content scales down or up when sheets appear, matching native iOS behavior
  • 👆 Gesture Dismissal: You drag sheets in multiple directions with haptic feedback at key interaction points
  • 📱 iOS-like Animations: Spring-based timing curves reproduce the feel of UIKit modal presentations
  • 🎨 Customizable Config: You adjust spring physics, scale factors, and drag thresholds per sheet instance
  • 🌗 Animation Modes: Choose between incremental scaling (background grows) or decremental scaling (background shrinks)
  • 🎯 Border Radius Sync: Corner radius animates in sync with drag position for smooth visual continuity
  • 🔍 Opacity Control: Sheet opacity responds to drag gestures with configurable fade behavior
  • 📐 Multi-Directional Drag: Enable dismissal from bottom, top, left, or right based on interaction patterns

Use Cases

  • Bottom Sheets: Implement interactive content panels that slide from the bottom.
  • Settings Panels: Create animated configuration menus with smooth transitions.
  • Detail Views: Present additional information with gesture-controlled dismissal.
  • Action Sheets: Build iOS-style action selectors with haptic feedback.

How to Use It

1. Install the core package and peer dependencies with a package manager you prefer:

npm install react-native-sheet-transitions react-native-reanimated react-native-gesture-handler

2. Wrap your application root with SheetProvider to initialize animation context:

The provider accepts global spring configuration and resize mode. All sheet instances inherit these defaults unless overridden locally.

import { SheetProvider } from 'react-native-sheet-transitions';
export default function App() {
  return (
    <SheetProvider
      springConfig={{ damping: 15, stiffness: 150, mass: 0.5 }}
      resizeType="decremental"
    >
      <Navigation />
    </SheetProvider>
  );
}

3. Create a basic modal using SheetScreen component:

The onClose callback fires when the sheet completes its dismiss animation. You handle navigation or state cleanup in this function.

import { SheetScreen } from 'react-native-sheet-transitions';
import { useRouter } from 'expo-router';
export default function ModalScreen() {
  const router = useRouter();
  return (
    <SheetScreen
      onClose={() => router.back()}
      dragDirections={{ toBottom: true }}
      opacityOnGestureMove={true}
      containerRadiusSync={true}
    >
      <View style={{ flex: 1, padding: 20 }}>
        <Text>Sheet Content</Text>
      </View>
    </SheetScreen>
  );
}

4. Expo Router Integration. Configure modal presentation in your layout file:

The transparent modal presentation prevents Expo Router from rendering default backgrounds that interfere with scale animations.

import { Stack } from 'expo-router';
import { SheetProvider } from 'react-native-sheet-transitions';
export default function Layout() {
  return (
    <SheetProvider>
      <Stack>
        <Stack.Screen name="index" />
        <Stack.Screen
          name="modal"
          options={{
            presentation: 'transparentModal',
            contentStyle: { backgroundColor: 'transparent' }
          }}
        />
      </Stack>
    </SheetProvider>
  );
}

5. Custom Animation Configuration. Override default spring physics and interaction thresholds:

Lower stiffness values create slower, more elastic animations. Higher mass increases momentum during gesture interactions.

<SheetScreen
  springConfig={{
    damping: 15,
    stiffness: 120,
    mass: 0.8
  }}
  scaleFactor={0.85}
  dragThreshold={100}
  opacityOnGestureMove={true}
  containerRadiusSync={true}
  initialBorderRadius={40}
  onClose={handleClose}
>
  <Content />
</SheetScreen>

6. Enable drag gestures from multiple edges:

You activate multiple directions for interfaces where users expect lateral swipe dismissal alongside vertical gestures.

<SheetScreen
  dragDirections={{
    toBottom: true,
    toLeft: true,
    toRight: true
  }}
  onClose={handleClose}
>
  <Content />
</SheetScreen>

7. Add blur or color overlays that fade with the sheet:

The background component receives automatic fade animations during open and close transitions. It positions absolutely behind sheet content without requiring manual layout calculations.

import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
<SheetScreen
  customBackground={
    <BlurView
      intensity={20}
      style={StyleSheet.absoluteFill}
    />
  }
  onClose={handleClose}
>
  <Content />
</SheetScreen>

8. Hook into animation phases for state management or haptic feedback:

The onCloseStart callback fires when drag distance exceeds the dismiss threshold. Use onBelowThreshold to detect when users drag past the threshold but release before completing dismissal.

import * as Haptics from 'expo-haptics';
<SheetScreen
  onOpenStart={() => {
    console.log('Sheet animation started');
  }}
  onOpenEnd={() => {
    console.log('Sheet fully visible');
  }}
  onCloseStart={() => {
    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
  }}
  onBelowThreshold={() => {
    console.log('User released before threshold');
  }}
  onCloseEnd={() => {
    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
    router.back();
  }}
>
  <Content />
</SheetScreen>

9. Configure background to scale up instead of down:

Incremental mode expands the background content as the sheet appears, creating an alternative visual style for certain design systems.

<SheetProvider resizeType="incremental">
  <App />
</SheetProvider>

10. Turn off scale effects on iOS or force disable on all platforms:

Background scaling runs by default on iOS only. Android and web skip this effect unless you override platform detection.

<SheetScreen
  disableRootScale={true}
  onClose={handleClose}
>
  <Content />
</SheetScreen>

11. Enable scroll detection for sheets containing long content:

The isScrollable prop prevents gesture conflicts between sheet drag and content scroll when users interact with scrollable regions.

<SheetScreen
  isScrollable={true}
  dragDirections={{ toBottom: true }}
  onClose={handleClose}
>
  <ScrollView>
    <LongContent />
  </ScrollView>
</SheetScreen>

API Reference

SheetProvider Props

PropTypeDefaultDescription
springConfigSpringConfig{ damping: 15, stiffness: 150, mass: 0.5 }Configuration for the spring animation physics.
resizeType'incremental' | 'decremental''decremental'Determines if the background scales up or down.
enableForWebbooleanfalseForces animations on web platforms (not recommended).

SheetScreen Props

PropTypeDefaultDescription
onClose() => voidRequiredFunction called when the sheet is dismissed.
scaleFactornumber0.83The scale value for the background content.
dragThresholdnumber150Pixel distance required to trigger a dismiss action.
springConfigSpringConfig(See Docs)Overrides the default spring configuration.
dragDirectionsDragDirections{ toBottom: true }Specifies which directions trigger drag gestures.
isScrollablebooleanfalseEnables scroll handling within the sheet content.
opacityOnGestureMovebooleanfalseChanges opacity during drag interactions.
containerRadiusSyncbooleantrueSyncs the border radius with the drag position.
initialBorderRadiusnumber50The starting border radius value.
styleViewStyleundefinedAdditional styles for the container.
disableSyncScaleOnDragDownbooleanfalseStops scale synchronization during downward drags.
customBackgroundReactNodeundefinedComponent that fades in behind the modal.
onOpenStart() => voidundefinedTriggered when the opening animation begins.
onOpenEnd() => voidundefinedTriggered when the opening animation finishes.
onCloseStart() => voidundefinedTriggered when a user gesture initiates a close.
onCloseEnd() => voidundefinedTriggered when the close animation finishes.
onBelowThreshold() => voidundefinedTriggered when drag returns below the threshold.
disableRootScalebooleanfalseDisables the background scaling effect entirely.
disableSheetContentResizeOnDragDownbooleanfalsePrevents sheet content from resizing during drag.

Related Resources

FAQs

Q: Does this library work with React Navigation?
A: Yes. Configure transparent modal presentation in your stack navigator and wrap the app with SheetProvider. The library operates independently of navigation systems.

Q: How do I prevent accidental dismissal on fast scrolls?
A: Set isScrollable={true} and increase dragThreshold to values like 200 or higher. This separates content scroll from sheet dismiss gestures.

Q: Can I disable animations on specific platforms?
A: Use disableRootScale={true} to disable background scaling. For full animation disable, conditionally render standard modals instead of SheetScreen on target platforms.

Q: Why does the sheet feel sluggish on Android?
A: Lower the mass value in springConfig and increase stiffness. Android performs best with damping 12, stiffness 180, mass 0.4.

Q: How do I customize the dismiss threshold per sheet?
A: Pass a different dragThreshold prop to each SheetScreen instance. Values between 100 and 200 work for most interaction patterns.

Add Comment