Skip to main content
Version: 3.x

Gesture composition & interactions

RNGH3 simplifies gesture interaction through dedicated composition hooks and configuration properties. To choose the right approach, simply ask: Are all the gestures attached to the same component?

  • If yes: Use composition hooks. These allow you to bundle multiple gestures—including previously composed ones—into a single object for a GestureDetector.

  • If no: Use relation properties to manually define how gestures interact. Since these properties also support composed gestures, you can mix both methods for more complex layouts.

Composition hooks

useCompetingGestures

Only one of the provided gestures can become active at the same time. The first gesture to become active will cancel the rest of the gestures. It accepts variable number of arguments.

For example, lets say that you have a component that you want to make draggable but you also want to show additional options on long press. Presumably you would not want the component to move after the long press activates. You can accomplish this using useCompetingGestures:

export default function App() {
const panGesture = usePanGesture({
onUpdate: () => {
console.log('Pan');
},
});
const longPressGesture = useLongPressGesture({
onDeactivate: (_, success) => {
if (success) {
console.log('Long Press');
}
},
});

const gesture = useCompetingGestures(panGesture, longPressGesture);

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={gesture}>
<View style={styles.box} />
</GestureDetector>
</GestureHandlerRootView>
);
}

useSimultaneousGestures

All of the provided gestures can activate at the same time. Activation of one will not cancel the other.

For example, if you want to make a gallery app, you might want user to be able to zoom, rotate and pan around photos. You can do it with useSimultaneousGestures:

Note: the useSharedValue and useAnimatedStyle are part of react-native-reanimated.

export default function App() {
const offset = useSharedValue({ x: 0, y: 0 });
const start = useSharedValue({ x: 0, y: 0 });

const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const rotation = useSharedValue(0);
const savedRotation = useSharedValue(0);

const animatedStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: offset.value.x },
{ translateY: offset.value.y },
{ scale: scale.value },
{ rotateZ: `${rotation.value}rad` },
],
};
});

const dragGesture = usePanGesture({
averageTouches: true,
onUpdate: (e) => {
offset.value = {
x: e.translationX + start.value.x,
y: e.translationY + start.value.y,
};
},
onDeactivate: () => {
start.value = {
x: offset.value.x,
y: offset.value.y,
};
},
});

const zoomGesture = usePinchGesture({
onUpdate: (e) => {
scale.value = savedScale.value * e.scale;
},
onDeactivate: () => {
savedScale.value = scale.value;
},
});

const rotationGesture = useRotationGesture({
onUpdate: (e) => {
rotation.value = savedRotation.value + e.rotation;
},
onDeactivate: () => {
savedRotation.value = rotation.value;
},
});

const composedGesture = useSimultaneousGestures(
dragGesture,
zoomGesture,
rotationGesture
);

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={composedGesture}>
<Animated.View style={[styles.box, animatedStyles]} />
</GestureDetector>
</GestureHandlerRootView>
);
}

useExclusiveGestures

Only one of the provided gestures can become active. Priority is determined by the order of the aguments, where the first gesture has the highest priority, and the last has the lowest. A gesture can activate only after all higher-priority gestures before it have failed.

For example, if you want to make a component that responds to single tap as well as to a double tap, you can accomplish that using useExclusiveGestures:

export default function App() {
const singleTap = useTapGesture({
onDeactivate: (_, success) => {
if (success) {
console.log('Single tap!');
}
},
});

const doubleTap = useTapGesture({
numberOfTaps: 2,
onDeactivate: (_, success) => {
if (success) {
console.log('Double tap!');
}
},
});

const taps = useExclusiveGestures(doubleTap, singleTap);

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={taps}>
<View style={styles.box} />
</GestureDetector>
</GestureHandlerRootView>
);
}

Cross-component interactions

requireToFail

requireToFail allows to delay activation of the handler until all handlers passed as arguments to this method fail (or don't begin at all).

For example, you may want to have two nested components, both of them can be tapped by the user to trigger different actions: outer view requires one tap, but the inner one requires 2 taps. If you don't want the first tap on the inner view to activate the outer handler, you must make the outer gesture wait until the inner one fails:

export default function App() {
const innerTap = useTapGesture({
numberOfTaps: 2,
onDeactivate: (_, success) => {
if (success) {
console.log('inner tap');
}
},
});

const outerTap = useTapGesture({
onDeactivate: (_, success) => {
if (success) {
console.log('outer tap');
}
},
requireToFail: innerTap,
});

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={outerTap}>
<View style={styles.outer}>
<GestureDetector gesture={innerTap}>
<View style={styles.inner} />
</GestureDetector>
</View>
</GestureDetector>
</GestureHandlerRootView>
);
}

block

block works similarly to requireToFail but the direction of the relation is reversed - instead of being one-to-many relation, it's many-to-one. It's especially useful for making lists where the ScrollView component needs to wait for every gesture underneath it. All that's required to do is to pass a ref, for example:

function Item({ backgroundColor, scrollGesture }: ItemProps) {
const scale = useSharedValue(1);
const zIndex = useSharedValue(1);

const pinch = usePinchGesture({
onBegin: () => {
zIndex.value = 100;
},
onUpdate: (e) => {
scale.value *= e.scaleChange;
},
onFinalize: () => {
scale.value = withTiming(1, undefined, (finished) => {
if (finished) {
zIndex.value = 1;
}
});
},
block: scrollGesture ?? undefined,
});

const animatedStyles = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
zIndex: zIndex.value,
}));

return (
<GestureDetector gesture={pinch}>
<Animated.View
style={[
{ backgroundColor: backgroundColor },
styles.item,
animatedStyles,
]}
/>
</GestureDetector>
);
}

export default function App() {
const [scrollGesture, setScrollGesture] = useState<NativeGesture | null>(
null
);

return (
<GestureHandlerRootView style={styles.container}>
<ScrollView
style={styles.container}
onGestureUpdate_CAN_CAUSE_INFINITE_RERENDER={(gesture) => {
if (!scrollGesture || scrollGesture.tag !== gesture.tag) {
setScrollGesture(gesture);
}
}}>
{ITEMS.map((item) => (
<Item
backgroundColor={item}
key={item}
scrollGesture={scrollGesture}
/>
))}
</ScrollView>
</GestureHandlerRootView>
);
}

simultaneousWith

simultaneousWith allows gestures across different components to be recognized simultaneously. For example, you may want to have two nested views, both with tap gesture attached. Both of them require one tap, but tapping the inner one should also activate the gesture attached to the outer view:

export default function App() {
const innerTap = useTapGesture({
onDeactivate: (_, success) => {
if (success) {
console.log('inner tap');
}
},
});

const outerTap = useTapGesture({
onDeactivate: (_, success) => {
if (success) {
console.log('outer tap');
}
},
simultaneousWith: innerTap,
});

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={outerTap}>
<View style={styles.outer}>
<GestureDetector gesture={innerTap}>
<View style={styles.inner} />
</GestureDetector>
</View>
</GestureDetector>
</GestureHandlerRootView>
);
}