Building a Draggable Sheet with React Native Reanimated 2

Swipe on, swipe off — mastering screen real-estate with this overlay component most commonly seen as an adjunct to a maps screen. This step-by-step tutorial covers the fundamentals of creating a draggable sheet in React Native using Reanimated 2 and Gesture Handler for a polished look and feel.

What is a draggable sheet?

Similar to a drawer or modal, a sheet is an overlay component used to display additional information or functionality on top of a main component. The sliding aspect takes advantage of the familiar swipe gesture, allowing the user to quickly show or hide content.

Whereas drawers may be used for navigation, and modals for a single action, sheets are suitable for nesting a variety of components, to display content (e.g., text, images) or actions (e.g., buttons, switches). There is a flow involved in the swiping up of the sheet, and the continuation into scrollable content. This essentially affords you two screens in one.

You may have seen an example when using navigation or ride-share apps.

Why Reanimated 2?

Software Mansion’s React Native Reanimated makes use of JSI, which fundamentally changes how or, more accurately, where your javascript is being processed. Instead of using the React Native bridge, JSI allows for the synchronous calling of native methods, providing direct access to the UI thread. This significantly reduces the time it takes for the UI to respond to events, meaning animation logic will be invoked on the current frame. The result is smooth and responsive animations.

Unfortunately, there are a few drawbacks with this library. In particular, it isn’t possible to use remote debugging for your animation worklet logic, as it is being run synchronously via a separate JavaScript VM (see JSI).

First steps

Below are instructions for setting up the starter template I’ve created. It includes some plumbing to help us get coding.

  • git clone https://github.com/jai-adapptor/sliding-sheet

  • yarn

  • npx pod-install

  • yarn ios to run on iOS simulator

  • Next, in the App directory, create a new folder called components. In here we will create three files: Sheet.tsx, MainContent.tsx and ExampleComponent.tsx

Sheet.tsx will hold the logic and UI for the sheet itself, and MainContent.tsx will be our base screen over which the sheet will slide. ExampleComponent just provides some content to nest inside the sheet. We will add these newly created components to index.tsx

Find the code for MainContent here, and ExampleComponent here, or feel free to create your own components to replace these placeholders.

Setting up the sheet

Now, for the main event, the sheet component. We will start by defining some types and getting screen dimensions for setting the component’s height.


The props defined here are optional, and allow you to override the three default ‘snap’ positions of the sheet. We are also setting a constant for the height of a navigation bar which will include a close button to dismiss the sheet when in its maximised position.

Here we are putting the screen’s dimensions inside a state, and using an event listener inside useEffect to update when the sizes change. We then define heights for the sheet’s three snap positions.

Reanimate it

Finally, we are getting into Reanimated territory. The useSharedValue hook is similar to a useState, but will be observed by Reanimated’s worklets, allowing the UI animations to respond to any changes to these values. The springConfig will determine how the spring animations look—feel free to play with these values until they look right to you. We have also added a DRAG_BUFFER constant to prevent the sheet from changing position until the user has dragged beyond a certain distance.

Now we need to handle the pan gesture, and change the height of the sheet in response to the user’s swiping motion, as well as potentially change the snap position of the sheet. Reanimated provides the useAnimatedGestureHandler hook, which works in conjunction with react-native-gesture-handler to capture gesture events that we can then use to update our animation values.

The three methods we use here are onStart, onActive and onEnd. onStart is used to update the context, which can be thought of as state on the UI thread. This lets us keep track of the last place a gesture ended, and prevent a jump back to default values between consecutive gestures. onActive is a simple one-liner that sets the height of the sheet based on the gesture and its previous state. onEnd is used to snap the sheet to one of the predefined heights depending on which direction the gesture is made and whether or not the gesture distance exceeds our DRAG_BUFFER. Whenever we want to explicitly set the height of the sheet, we wrap the height value in withSpring using the config we defined earlier to animate.

In order to pass the animatable styles to our components, we make use of the useAnimatedStyle hook, which will return a style object that is reactive to any of our shared values. This hook automatically provides the functionality of a worklet directive, meaning it will be executed on the UI thread.

Note: it is more performant to only include styles that are going to be animated in this hook. Additional styling should be passed to the component separately.

This is how the markup will look. Notice that we make use of Animated.View for any components that are taking in animated style props, and wrap the component in a PanGestureHandler to capture user gestures.

And here, finally, are our styles:

Outro

And that’s it for our sheet component. See the code for the component here.

For a full working demo, check out the demo branch.

Slidable sheet example
Previous
Previous

RTK Query: a better way to Redux

Next
Next

Into the … Adapptor-verse?