Most apps nowadays allow users to interact with key components by swiping, double-tapping, pinching, long-pressing, etc. When properly implemented in your mobile app, gestures provide your users with an engaging, natural, and intuitive experience. In fact, you can replace most visible controls, such as buttons and icon clicks, with gestures.
There are several packages you can use to implement gestures in a React Native app. The most popular and recommended library is react-native-gesture-handler
. This library exposes platform-specific (i.e., Android and iOS) native touch and gestures to React Native.
Although gestures in React Native can be handled using the built-in Gesture Responder System, this implementation has some limitations because it runs on the JavaScript thread. So every time a gesture event is carried out, it sends the data across the React Native bridge to the interface, which can lead to poor performance.
React Native Gesture Handler enables you to implement highly performant gestures in React Native because it runs in the native thread and follows platform-specific behavior, which in turn leads to better performance.
The React Native Gesture Handler library comes with lots of useful gestures, including:
PanGestureHandler
TapGestureHandler
LongPressGestureHandler
PinchGestureHandler
In addition to the above gestures, we’ll also demonstrate how to implement the pull-to-refresh, swipeable, and double-tap gestures.
Initializing a new Expo app
We’ll start by initializing a new Expo app. If you don’t have Expo installed on your machine already, run the command below:
# installing expo CLI globally npm install --global expo-cli
To create a new Expo app, run the following command on your terminal
# Create a project named react-native-gestures expo init react-native-gestures
Navigate to your project directory and run the following command to start your application:
expo start
Press i
to open in an iOS simulator, or press a
to open in an Android emulator or connected device. Make sure your simulator or emulator is set up already.
Now let’s get started implementing and managing gestures in our React Native app.
Pan gesture
To implement a pan gesture in React Native using the react-native-gesture-handler
library, we’ll use PanGestureHandler
. PanGestureHandler
is a continuous gesture handler that generates streams of gesture events when the user pans (drags) an element.
To get started with PanGestureHandler
, we have to import it from the react-native-gesture-handler
library we installed earlier:
import { PanGestureHandler } from 'react-native-gesture-handler';
Next, we need to wrap the element to which we want to apply the pan gesture with the PanGestureHandler
component we imported earlier:
// in your return block <PanGestureHandler> <View style={styles.square}/> </PanGestureHandler>
To make things move around, we need to use the base gesture handler props called onGestureEvent
and pass a callback function to it.
Let’s create a function, which we’ll later pass to the onGestureEvent
:
// dont't forget to import Animated from react native onPanGestureEvent = Animated.event( [ { nativeEvent: { translationX: this.translateX, translationY: this.translateY, }, }, ], { useNativeDriver: true } );
Note that we’re using a class component here.
Let’s pass the function we just created to the onGestureEvent
prop in the PanGestureHandler
component.
<PanGestureHandler onGestureEvent={this.onPanGestureEvent}> </PanGestureHandler>
Since we want our View
component to animate, let’s replace it with Animated.View
and add the translateX
and translateY
properties in our transform
array to specify the translation of the pan gesture along the X and Y axes.
<Animated.View style={[ styles.square, { transform: [ { translateX: this.translateX, }, { translateY: this.translateY, }, ], }, ]} />

Here’s the complete code for creating pan gestures with React Native Gesture Handler:
import React, { Component } from 'react'; import { StatusBar } from 'expo-status-bar'; import { PanGestureHandler } from 'react-native-gesture-handler'; import { Animated, StyleSheet, Text } from 'react-native'; export default class PanGesture extends Component { translateX = new Animated.Value(0); translateY = new Animated.Value(0); onPanGestureEvent = Animated.event( [ { nativeEvent: { translationX: this.translateX, translationY: this.translateY, }, }, ], { useNativeDriver: true } ); render() { return ( <> <Text>Pan Gesture Handler</Text> <PanGestureHandler onGestureEvent={this.onPanGestureEvent}> <Animated.View style={[ styles.square, { transform: [ { translateX: this.translateX, }, { translateY: this.translateY, }, ], }, ]} /> </PanGestureHandler> <StatusBar style="auto" /> </> ); } } const styles = StyleSheet.create({ square: { width: 150, height: 150, backgroundColor: '#28b5b5', marginTop: 22, }, });
TapGestureHandler
With TapGestureHandler
, we can implement both single- and double-tap gestures. Let’s start with single-tap gesture and then move on to a double-tap gesture.
Single-tap gesture

TapGestureHandler
using React Native Gesture Handler.We’ll implement the single-tap gesture using the TapGestureHandler
component. Notice we’re adding onHandlerStateChange
props and passing a function:
import { View, StyleSheet, Text } from 'react-native'; import { TapGestureHandler, State } from 'react-native-gesture-handler'; export default function TapGesture() { const onSingleTapEvent = (event) => { if (event.nativeEvent.state === State.ACTIVE) { alert('Hey single tap!'); } }; return ( <> <Text>Double and Single Tap Gesture Handler</Text> <TapGestureHandler onHandlerStateChange={onSingleTapEvent} waitFor={doubleTapRef} > <View style={styles.square} /> </TapGestureHandler> </> ); }
Double-tap

TapGestureHandler
using React Native Gesture Handler.The major difference between the single- and double-tap gesture implementation is the numberOfTaps
props, which is passed to the TapGestureHandler
component. Notice we’re passing 2
to numberOfTaps
to specify that we want to trigger the onHandlerStateChange
event callback:
import React, { useRef, useState } from 'react'; import { View, StyleSheet, Text } from 'react-native'; import { TapGestureHandler, State } from 'react-native-gesture-handler'; export default function TapGesture() { const [likeColour, setLikeColour] = useState('#28b5b5'); const doubleTapRef = useRef(null); const onDoubleTapEvent = (event) => { if (event.nativeEvent.state === State.ACTIVE) { likeColour === '#28b5b5' ? setLikeColour('red') : setLikeColour('#28b5b5'); } }; const styles = StyleSheet.create({ square: { width: 150, height: 150, backgroundColor: likeColour, marginTop: 22, marginBottom: 22, }, }); return ( <> <Text>Double and Single Tap Gesture Handler</Text> <TapGestureHandler ref={doubleTapRef} onHandlerStateChange={onDoubleTapEvent} numberOfTaps={2} > <View style={styles.square} /> </TapGestureHandler> </> ); }
Note that we’re using a functional component here because we’re using the useRef
hook.
Implementing single- and double-tap in one component

TapGestureHandler
using React Native Gesture Handler.With TapGestureHandler
, we can implement a gesture where an event is called on single-tap and another on double-tap. A good example of this is the LinkedIn mobile app, where a single click opens the post and a double-click likes the post.
import React, { useRef, useState } from 'react'; import { View, StyleSheet, Text } from 'react-native'; import { TapGestureHandler, State } from 'react-native-gesture-handler'; export default function TapGesture() { const [likeColour, setLikeColour] = useState('#28b5b5'); const doubleTapRef = useRef(null); const onSingleTapEvent = (event) => { if (event.nativeEvent.state === State.ACTIVE) { alert('Hey single'); } }; const onDoubleTapEvent = (event) => { if (event.nativeEvent.state === State.ACTIVE) { likeColour === '#28b5b5' ? setLikeColour('red') : setLikeColour('#28b5b5'); } }; const styles = StyleSheet.create({ square: { width: 150, height: 150, backgroundColor: likeColour, marginTop: 22, marginBottom: 22, }, }); return ( <> <Text>Double and Single Tap Gesture Handler</Text> <TapGestureHandler onHandlerStateChange={onSingleTapEvent} waitFor={doubleTapRef} > <TapGestureHandler ref={doubleTapRef} onHandlerStateChange={onDoubleTapEvent} numberOfTaps={2} > <View style={styles.square} /> </TapGestureHandler> </TapGestureHandler> </> ); }
To make this work, we need to add the waitFor={doubleTapRef}
to the single tap TapGestureHandler
component. When the doubleTapRef
is truthy, the on onSingleTapEvent
will not be called.
Swipeable gesture

Swipeable
component using React Native Gesture Handle.To demonstrate how to implement a swipeable gesture, let’s create a list of items where users can swipe right or left and certain events or methods are called.
Let’s create a flat list component and pass in our data to the data
props:
<FlatList data={todoList} keyExtractor={(item) => item.id} renderItem={({ item }) => <ListItem {...item} />} ItemSeparatorComponent={() => <Separator />} />
Notice that the renderItem
props is returning a ListItem
component. This represents our list todo items, as shown in the demo above.
Now let’s create our ListItem
component and make it swipeable.
First, import the Swipeable
component from the react-native-gesture-handler
package:
import Swipeable from 'react-native-gesture-handler/Swipeable';
Next, wrap the View
and Text
components with the Swipeable
component we imported earlier. With this in place, our component is swipeable. But in a real-world scenario, you would want some kind of event or function to be called when the user swipes left or right.
To render containers on the left, use the renderLeftActions
props and pass in a component. To render containers on the right, use the renderRightActions
props.
On swipe to left, we have the delete
container and on swipe to right, we have the bookmark
container. The renderLeftActions
and renderRightActions
props make this possible.
The onSwipeableRightOpen
props accepts a method that is called when the swipeable gesture from right to left is complete — i.e., when the right action panel is opened. The onSwipeableLeftOpen
props accepts a method that is called when the left action panel is opened.
const ListItem = ({ text }) => ( <Swipeable renderLeftActions={LeftSwipeActions} renderRightActions={rightSwipeActions} onSwipeableRightOpen={swipeFromRightOpen} onSwipeableLeftOpen={swipeFromLeftOpen} > <View style={{ paddingHorizontal: 30, paddingVertical: 20, backgroundColor: 'white', }} > <Text style={{ fontSize: 24 }} style={{ fontSize: 20 }}> {text} </Text> </View> </Swipeable> );
Here is the full code for the swipeable gesture:
import React from 'react'; import { SafeAreaView, StyleSheet, View, Text, StatusBar, FlatList, } from 'react-native'; import Swipeable from 'react-native-gesture-handler/Swipeable'; const todoList = [ { id: '1', text: 'Learn JavaScript' }, { id: '2', text: 'Learn React' }, { id: '3', text: 'Learn TypeScript' }, ]; const Separator = () => <View style={styles.itemSeparator} />; const LeftSwipeActions = () => { return ( <View style={{ flex: 1, backgroundColor: '#ccffbd', justifyContent: 'center' }} > <Text style={{ color: '#40394a', paddingHorizontal: 10, fontWeight: '600', paddingHorizontal: 30, paddingVertical: 20, }} > Bookmark </Text> </View> ); }; const rightSwipeActions = () => { return ( <View style={{ backgroundColor: '#ff8303', justifyContent: 'center', alignItems: 'flex-end', }} > <Text style={{ color: '#1b1a17', paddingHorizontal: 10, fontWeight: '600', paddingHorizontal: 30, paddingVertical: 20, }} > Delete </Text> </View> ); }; const swipeFromLeftOpen = () => { alert('Swipe from left'); }; const swipeFromRightOpen = () => { alert('Swipe from right'); }; const ListItem = ({ text }) => ( <Swipeable renderLeftActions={LeftSwipeActions} renderRightActions={rightSwipeActions} onSwipeableRightOpen={swipeFromRightOpen} onSwipeableLeftOpen={swipeFromLeftOpen} > <View style={{ paddingHorizontal: 30, paddingVertical: 20, backgroundColor: 'white', }} > <Text style={{ fontSize: 24 }} style={{ fontSize: 20 }}> {text} </Text> </View> </Swipeable> ); const SwipeGesture = () => { return ( <> <StatusBar /> <SafeAreaView style={styles.container}> <Text style={{ textAlign: 'center', marginVertical: 20 }}> Swipe right or left </Text> <FlatList data={todoList} keyExtractor={(item) => item.id} renderItem={({ item }) => <ListItem {...item} />} ItemSeparatorComponent={() => <Separator />} /> </SafeAreaView> </> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, itemSeparator: { flex: 1, height: 1, backgroundColor: '#444', }, }); export default SwipeGesture;
Hold/long-press gesture

LongPressGestureHandler
using React Native Gesture Handle.With React Native Gesture Handler, implementing the long press/hold gesture is straightforward.
Basically, we need to wrap the React Native component we want to implement the gesture on with LongPressGestureHandler
, which is imported from react-native-gesture-handler
, and then add the onHandlerStateChange
props, which triggers a method when the user holds on the component for a given duration. To specify the duration of the long press, we pass in the minDurationMs
, which accepts a number expressed in milliseconds.
Here is the code for the demo shown above:
import React from 'react'; import { View, StyleSheet } from 'react-native'; import { LongPressGestureHandler, State } from 'react-native-gesture-handler'; export default function LongPressGesture() { const onLongPress = (event) => { if (event.nativeEvent.state === State.ACTIVE) { alert("I've been pressed for 800 milliseconds"); } }; return ( <LongPressGestureHandler onHandlerStateChange={onLongPress} minDurationMs={800} > <View style={styles.box} /> </LongPressGestureHandler> ); } const styles = StyleSheet.create({ box: { width: 150, height: 150, backgroundColor: '#28b5b5', marginTop: 22, marginBottom: 22, }, });
Pinch-to-zoom
Just like the name implies, this is a continuous gesture that tracks the distance between two fingers and uses that information to scale or zoom your content.
render() { return ( <PinchGestureHandler onGestureEvent={this.onPinchGestureEvent} onHandlerStateChange={this.onPinchHandlerStateChange} > <Animated.View style={[ styles.pinchableImage, { transform: [{ perspective: 1 }, { scale: this.scale }], }, ]} ></Animated.View> </PinchGestureHandler> ); }

PinchGestureHandler
using React Native Gesture Handle.To make this work, we need to add the onGestureEvent
props to our PinchGestureHandler
and then set the { scale: this.scale }
object in our transform
array:
import React, { Component } from 'react'; import { View, Image, StyleSheet, Animated } from 'react-native'; import { PinchGestureHandler, State } from 'react-native-gesture-handler'; export default class PinchToZoom extends Component { baseScale = new Animated.Value(1); pinchScale = new Animated.Value(1); scale = Animated.multiply(this.baseScale, this.pinchScale); lastScale = 1; onPinchGestureEvent = Animated.event( [{ nativeEvent: { scale: this.pinchScale } }], { useNativeDriver: true } ); onPinchHandlerStateChange = (event) => { if (event.nativeEvent.oldState === State.ACTIVE) { this.lastScale *= event.nativeEvent.scale; this.baseScale.setValue(this.lastScale); this.pinchScale.setValue(1); } }; render() { return ( <PinchGestureHandler onGestureEvent={this.onPinchGestureEvent} onHandlerStateChange={this.onPinchHandlerStateChange} > <Animated.View style={[ styles.pinchableImage, { transform: [{ perspective: 1 }, { scale: this.scale }], }, ]} ></Animated.View> </PinchGestureHandler> ); } } const styles = StyleSheet.create({ pinchableImage: { width: 250, height: 250, backgroundColor: '#28b5b5', marginTop: 22, marginBottom: 22, }, });
Pull-to-refresh
To implement pull-to-refresh in React Native, you don’t need an external library. Just add the onRefresh
props, which accepts a function in the FlatList
component and also sets the refreshing
props to a boolean.
// at the top of your component const [refreshing, setrefreshing] = useState(false); <FlatList data={data} renderItem={renderItem} keyExtractor={(item) => item.id} refreshing={refreshing} onRefresh={onRefresh} />
By default, we’re setting refreshing
to false
and then, when the onRefresh
method is called, we set the refreshing
state to true
using setrefreshing
:

Here’s the full code for the demo above:
import React, { useState } from 'react'; import { SafeAreaView, View, FlatList, StyleSheet, Text, StatusBar, } from 'react-native'; const DATA = [ { id: '1', title: 'Take coffee', }, { id: '2', title: 'Write some code', }, { id: '3', title: 'Take the test', }, { id: '4', title: 'Excercise', }, ]; const Item = ({ title }) => ( <View style={styles.item}> <Text style={styles.title}>{title}</Text> </View> ); export default function PullToRefresh() { const [refreshing, setrefreshing] = useState(false); const [data, setdata] = useState(DATA); const onRefresh = () => { setrefreshing(true); setTimeout(() => { setdata((data) => [ ...data, { id: '57878', title: 'Take a walk in the park', }, ]); setrefreshing(false); }, 2000); }; const renderItem = ({ item }) => <Item title={item.title} />; return ( <SafeAreaView style={styles.container}> <FlatList data={data} renderItem={renderItem} keyExtractor={(item) => item.id} refreshing={refreshing} onRefresh={onRefresh} /> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, marginTop: StatusBar.currentHeight || 0, }, item: { backgroundColor: '#fad586', padding: 20, marginVertical: 8, marginHorizontal: 16, }, title: { fontSize: 20, }, });
Conclusion
Implementing gestures in React Native can help improve the user experience and make your app feel natural to users.
In this tutorial, we covered the implementation and management of gestures in a React Native app, including swipeable, pan, double- and single-tap, pinch-to-zoom, and more.
All the code for this demo is available on GitHub.
LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
What is the difference between LongPressGestureHandler and onLongPress event handler in TouchableWithoutFeedback?
Muito obrigado, ajudou muito o seu post!