Core Concepts

Gesture Ownership

Understand how gesture directions are claimed across nested navigators, snap sheets, and scroll boundaries.

What Ownership Means

When more than one transition-enabled screen could respond to the same drag, the library resolves ownership before the pan gesture activates.

Ownership Is Per Direction

The gesture system tracks these directions independently:

  • vertical
  • vertical-inverted
  • horizontal
  • horizontal-inverted

That means:

  • horizontal and vertical never compete with each other
  • vertical and vertical-inverted are different claims
  • horizontal and horizontal-inverted are different claims
  • bidirectional expands to all four directions

Resolution Order

For a given drag direction, ownership resolves in this order:

  1. If the current screen claims that direction, it owns the gesture.
  2. Otherwise, the library walks up the ancestor chain and finds the nearest ancestor that claims it.
  3. If no one claims it, the gesture does nothing.

This is why a screen can own one direction locally and still inherit another from an ancestor.

Shadowing

If a child screen and an ancestor both claim the same direction, the child wins for that direction.

Two implementation details matter here:

  • only the topmost route in the child navigator registers a shadowing claim
  • once that child starts dismissing, the ancestor is allowed to activate again

In practice, shadowing is what prevents two nested navigators from reacting to the same drag at the same time.

Snap Sheets Claim Both Directions on Their Axis

A screen with snapPoints claims both directions on its axis.

TSX

1<Stack.Screen
2 name="Sheet"
3 component={SheetScreen}
4 options={{
5 gestureEnabled: true,
6 gestureDirection: "vertical",
7 snapPoints: [0.5, 0.8],
8 initialSnapIndex: 0,
9 }}
10/>

A vertical sheet above owns:

  • vertical
  • vertical-inverted

A horizontal sheet above owns:

  • horizontal
  • horizontal-inverted

This is why the same sheet can expand in one direction and collapse or dismiss in the other.

gestureEnabled: false only disables dismiss at the minimum detent. If snapPoints still exist, the sheet can continue to own its axis for snap navigation.

Scroll Boundary Handoff

Use Transition.ScrollView or Transition.FlatList when scroll content needs to cooperate with navigator gestures.

For non-sheet screens, a scrollable yields only at the boundary for the active direction:

DirectionBoundary
verticaltop (scrollY <= 0)
vertical-invertedbottom (scrollY >= maxY)
horizontalleft (scrollX <= 0)
horizontal-invertedright (scrollX >= maxX)

Axis isolation still applies:

  • a vertical scroll view does not hand off to horizontal gestures
  • a horizontal scroll view does not hand off to vertical gestures

For snap sheets, the handoff is controlled by sheetScrollGestureBehavior:

  • "expand-and-collapse": boundary drags can expand and collapse the sheet
  • "collapse-only": boundary drags can collapse or dismiss, but expansion must start from dead space outside the scrollable

TSX

1<Stack.Screen
2 name="Sheet"
3 component={SheetScreen}
4 options={{
5 gestureEnabled: true,
6 gestureDirection: "vertical",
7 snapPoints: [0.4, 0.7, 1],
8 sheetScrollGestureBehavior: "collapse-only",
9 }}
10/>
11
12function SheetScreen() {
13 return (
14 <Transition.ScrollView>
15 <LongContent />
16 </Transition.ScrollView>
17 );
18}

If no screen in the current ownership chain claims that direction, the scroll view just keeps the gesture.

Real Nested Navigator Patterns

Different Directions Coexist

This is the cleanest setup: the parent owns one direction, the child screen owns another.

TSX

1import { createBlankStackNavigator } from "react-native-screen-transitions/react-navigation";
2
3const RootStack = createBlankStackNavigator();
4const DetailsStack = createBlankStackNavigator();
5
6function DetailsNavigator() {
7 return (
8 <DetailsStack.Navigator>
9 <DetailsStack.Screen name="List" component={ListScreen} />
10 <DetailsStack.Screen
11 name="Card"
12 component={CardScreen}
13 options={{
14 gestureEnabled: true,
15 gestureDirection: "horizontal",
16 }}
17 />
18 </DetailsStack.Navigator>
19 );
20}
21
22function AppNavigator() {
23 return (
24 <RootStack.Navigator>
25 <RootStack.Screen name="Home" component={HomeScreen} />
26 <RootStack.Screen
27 name="Details"
28 component={DetailsNavigator}
29 options={{
30 gestureEnabled: true,
31 gestureDirection: "vertical",
32 }}
33 />
34 </RootStack.Navigator>
35 );
36}

On Card:

  • horizontal is owned locally by Card
  • vertical can still resolve to the ancestor Details route

Same Direction Shadows the Ancestor

If the child screen also claims vertical, it shadows the ancestor's vertical claim while that child is active.

TSX

1<DetailsStack.Screen
2 name="Card"
3 component={CardScreen}
4 options={{
5 gestureEnabled: true,
6 gestureDirection: "vertical",
7 }}
8/>

Now the active child screen owns vertical itself, so the ancestor does not handle the same swipe.

Snap Sheet Shadows Its Axis

If the child screen is a vertical snap sheet, it owns both vertical and vertical-inverted, so a same-axis ancestor no longer receives either direction while the sheet is active.

TSX

1<DetailsStack.Screen
2 name="Sheet"
3 component={SheetScreen}
4 options={{
5 gestureEnabled: true,
6 gestureDirection: "vertical",
7 snapPoints: [0.5, 0.8],
8 }}
9/>

If the sheet instead uses gestureDirection: "horizontal", it owns the horizontal axis and can still inherit vertical ownership from an ancestor.

useScreenGesture()

useScreenGesture() returns a gesture ref so you can coordinate your own Gesture Handler gestures with the navigator gesture.

Valid targets are:

  • useScreenGesture()
  • useScreenGesture({ depth: 0 })
  • useScreenGesture({ depth: -1 })
  • useScreenGesture({ depth: -2 })

TSX

1const parentGesture = useScreenGesture({ depth: -1 });
2
3const pan = Gesture.Pan().requireExternalGestureToFail(parentGesture);

depth: 0 resolves the current screen gesture. Negative depths walk ancestors. Ancestor targeting stops at isolation boundaries, so a negative depth may resolve to null once the lookup reaches an isolated tree edge.