Skip to main content

Overview

In nested navigation hierarchies, multiple navigators may want to handle gestures on the same axis (horizontal or vertical). Gesture ownership ensures only one navigator controls a given direction, preventing conflicts and confusion.

Core Principle: Per-Direction Ownership

Each gesture axis has exactly one owner:
Parent Navigator
├─ Claims vertical gesture
└─ Child Navigator
   └─ Claims horizontal gesture
   └─ Screen
  • Parent owns vertical drag (can dismiss child)
  • Child owns horizontal drag (can navigate between siblings)
  • Both work simultaneously without conflict

Child Shadows Parent

When a child navigator and parent both want to handle the same axis, the child wins:
Parent Navigator (vertical gesture)
└─ Child Navigator (vertical gesture)
   └─ Screen
   └─ Another Screen

Result: Child owns vertical. Parent cannot vertically dismiss.
This is called “shadowing.” The child’s gesture claim shadows the parent’s on that axis.

Gesture Inheritance

When a child doesn’t explicitly claim a gesture, it inherits the parent’s:
Parent Navigator (vertical gesture enabled)
└─ Child Navigator (no explicit gesture)
   └─ Screen

Result: Child inherits parent's vertical gesture.
Any gesture initiated on the child is handled by the parent.

Snap Points Claim Ownership

Defining snap points automatically claims ownership of that axis:
<Transition.Modal
  snapPoints={[0.4, 1]}  // Claims vertical ownership
  gestureEnabled
>
  {/* content */}
</Transition.Modal>
A parent navigator cannot vertically dismiss a child that has snap points defined, even if the parent also has gestureDirection: "vertical".

Practical Examples

Two-Axis Navigation

Parent handles one axis, child handles another—no conflicts:
// Parent: Horizontal tabs
<Transition.Stack gestureDirection="horizontal" gestureEnabled>
  <Transition.Screen name="Tab1" component={Tab1} />
  <Transition.Screen name="Tab2" component={Tab2} />

  {/* Child: Vertical sheets */}
  <Transition.Modal
    name="Sheet"
    gestureDirection="vertical"
    gestureEnabled
    snapPoints={[0.5, 1]}
  >
    <SheetContent />
  </Transition.Modal>
</Transition.Stack>
Result:
  • Parent claims horizontal (slide between tabs)
  • Child claims vertical (tap to expand sheet, swipe to dismiss)
  • Both work simultaneously ✓

Same-Axis Shadowing

Child sheet shadows parent’s vertical gesture:
// Parent: Vertical stack
<Transition.Stack gestureDirection="vertical" gestureEnabled>
  <Transition.Screen name="Home" component={Home} />

  {/* Child: Also vertical */}
  <Transition.Modal
    name="Details"
    gestureDirection="vertical"
    gestureEnabled
    snapPoints={[0.5, 1]}
  >
    <DetailsContent />
  </Transition.Modal>
</Transition.Stack>
Result:
  • Parent initially claims vertical
  • When Details sheet opens, child claims vertical
  • Parent cannot vertically dismiss Details
  • Swipe down on Details closes it (child’s gesture)
  • To return to Home, use Back button or tap header
This prevents accidental dismissal conflicts.

ScrollView at Boundary

ScrollView gestures coordinate with navigator gestures using the boundary concept:
<Transition.Modal
  snapPoints={[0.4, 1]}
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <ScrollView>
    <LongContent />
  </ScrollView>
</Transition.Modal>
Behavior:
  • Scrolling when ScrollView is NOT at top: ScrollView owns the gesture
  • Scrolling when ScrollView IS at top (scrollY = 0): Gesture transfers to sheet
    • With "expand-and-collapse": Drag up expands the sheet
    • With "collapse-only": Drag has no effect; use dead space to expand
The ScrollView “yields” ownership to the navigator at the boundary.

Stack Dismissal Ownership

The topmost screen in each navigator claims dismissal:
Parent Stack (Screen A, Screen B)
└─ Child Stack (Screen X, Screen Y)

When Screen Y is visible:
- Parent cannot dismiss (Screen B is not topmost in Parent)
- Child can dismiss Screen Y (topmost in Child)
This prevents accidental dismissal of nested stacks.

Nested Scenarios

Scenario: Detail Stack Inside Tab Navigation

<Transition.BottomTabs
  // Horizontal, swipe between tabs
  gestureDirection="horizontal"
  gestureEnabled
>
  <Transition.Screen name="HomeTab" component={HomeTab} />

  <Transition.Screen name="DetailsTab">
    <Transition.Stack
      // Vertical, each detail screen dismisses upward
      gestureDirection="vertical"
      gestureEnabled
    >
      <Transition.Screen name="Detail1" component={Detail1} />
      <Transition.Screen name="Detail2" component={Detail2} />
    </Transition.Stack>
  </Transition.Screen>
</Transition.BottomTabs>
Ownership:
  • Tabs claim horizontal (swipe left/right to switch)
  • Detail Stack claims vertical (swipe up to go back)
  • Both work simultaneously

Scenario: Paged Sheets with Modal

<Transition.Stack>
  <Transition.Screen name="Home" component={Home} />

  <Transition.Modal
    name="PagedSheet"
    snapPoints={[0.5, 1]}
    gestureEnabled
    // Has horizontal paging inside
  >
    <Transition.Stack
      gestureDirection="horizontal"
      gestureEnabled
    >
      <Transition.Screen name="Page1" component={Page1} />
      <Transition.Screen name="Page2" component={Page2} />
    </Transition.Stack>
  </Transition.Modal>
</Transition.Stack>
Ownership:
  • Modal claims vertical (snap points)
  • Paged Stack claims horizontal (swipe between pages)
  • Modal gesture is shadowed from parent (parent can’t dismiss)
  • Both modal and page navigation work

Advanced: Ancestor Targeting

Use hooks to explicitly set gesture ownership:
import { useScreenGesture } from 'react-native-screen-transitions';

function DetailScreen() {
  // This screen's gesture is owned by parent
  useScreenGesture("parent", {
    gestureEnabled: true,
    gestureDirection: "vertical",
  });

  return (
    <ScrollView>
      {/* content */}
    </ScrollView>
  );
}
Options:
  • "self": Gesture on this screen
  • "parent": Gesture owned by parent navigator
  • "root": Gesture owned by root navigator
  • { ancestor: 2 }: Gesture owned by ancestor at level 2
This is useful when you want to override the default ownership rules.

Troubleshooting Ownership

”Parent gesture doesn’t work; child shadows it”

This is intentional. A child navigator’s gesture claims the axis. To re-enable parent gesture:
// Option 1: Use different axes
<Transition.Stack gestureDirection="vertical">  {/* Parent */}
  <Transition.Stack gestureDirection="horizontal"> {/* Child */}
    {/* Screens */}
  </Transition.Stack>
</Transition.Stack>

// Option 2: Disable child gesture
<Transition.Stack gestureEnabled={false}>
  {/* No shadowing */}
</Transition.Stack>

“Child gesture not responding”

Parent may be claiming the axis. Check:
  1. Parent’s gestureDirection: Ensure it’s different from child
  2. Parent’s gestureEnabled: Disable if not needed
  3. Snap points: Verify child has snap points if using Modal with vertical snap

”ScrollView scroll and navigator gesture conflict”

Use sheetScrollGestureBehavior to define the boundary:
<Transition.Modal
  sheetScrollGestureBehavior="collapse-only"
  // ScrollView scroll doesn't expand; only dismiss works
>
  <ScrollView>
    {/* content */}
  </ScrollView>
</Transition.Modal>

Best Practices

  1. Use different axes when possible: Parent horizontal, child vertical
  2. Snap points implicitly claim ownership: No need to duplicate gesture props
  3. Test with nested navigators: Verify both levels respond as expected
  4. ScrollView at boundaries: Use sheetScrollGestureBehavior to avoid confusion
  5. Document gesture ownership in complex hierarchies to avoid surprises

Visual Reference

📱 Device
├─ App (Root)
│  ├─ BottomTabs (→ horizontal)
│  │  ├─ HomeTab
│  │  │  └─ HomeStack (↑ vertical)
│  │  │     ├─ Home screen
│  │  │     └─ Detail screen
│  │  │
│  │  └─ SettingsTab
│  │     └─ SettingsView
│  │
│  └─ Modal (↕ vertical snap)
│     ├─ Sheet content
│     └─ ScrollView
│        └─ Content at scroll boundary
Legend:
  • horizontal gesture
  • vertical snap/gesture
  • Child gestures shadow parent on same axis

Next Steps