Core Concepts

Gestures

Configure dismiss gestures, snap sheets, and gesture hooks from screen options.

Where Gesture Config Lives

TSX

1<Stack.Screen
2 name="Detail"
3 component={DetailScreen}
4 options={{
5 gestureEnabled: true,
6 gestureDirection: "vertical",
7 screenStyleInterpolator,
8 }}
9/>

Supported Directions

gestureDirection accepts pan and pinch directions:

  • "horizontal"
  • "horizontal-inverted"
  • "vertical"
  • "vertical-inverted"
  • "bidirectional"
  • "pinch-in"
  • "pinch-out"
  • an array of pan and pinch directions
  • structured entries such as { gesture: "vertical", area: "edge" }

For non-snap screens, each direction in the array can activate dismissal. For screens with snapPoints, the array defines which pan axes and pinch direction can drive the same snap state. The first direction on a pan axis controls that axis's collapse polarity, and the first pinch direction controls whether pinch-in or pinch-out collapses.

Pan entries can include an activation area:

TSX

1options={{
2 gestureDirection: [
3 { gesture: "horizontal", area: "edge" },
4 { gesture: "vertical", area: 32 },
5 "pinch-in",
6 ],
7}}

area accepts "screen", "edge", or a numeric edge distance in points. Pinch entries ignore area.

Rotation is not a gestureDirection value. When a screen enables pinch-in or pinch-out, rotation runs alongside pinch and publishes gesture.rotation.

Gesture Options

gestureEnabled

Enables swipe-to-dismiss. For screens with snapPoints, snapping between non-dismiss detents can still work when this is false.

typedefaultrequired
booleanundefinedNo

gestureDirection

Dismiss direction, pan axis, pinch direction, or ordered direction list.

typedefaultrequired
GestureDirectionEntry | GestureDirectionEntry[]"horizontal"No

gestureTracking

Controls whether the screen tracks live gesture values.

typedefaultrequired
"auto" | "never" | "always""auto"No

gestureSensitivity

Multiplies live gesture movement before it drives progress and non-raw gesture values.

typedefaultrequired
number1No

gestureVelocityImpact

Release velocity influence on dismiss decisions.

typedefaultrequired
number0.3No

gestureSnapVelocityImpact

Release velocity influence on snap-target selection.

typedefaultrequired
number0.1No

gestureSnapLocked

Locks gesture-driven snap movement to the current detent.

typedefaultrequired
booleanfalseNo

sheetSnapBehavior

Controls how a drag maps movement between snap points. "continuous" maps movement directly to global progress and can cross multiple snap points. "step" gives the adjacent interval a full normalized drag range and limits each gesture to one snap-point step.

typedefaultrequired
"continuous" | "step""continuous"No

gestureReleaseVelocityScale

Multiplies normalized release velocity before the spring runs.

typedefaultrequired
number1No

sheetScrollGestureBehavior

Scroll-boundary handoff mode for snap sheets.

typedefaultrequired
"expand-and-collapse" | "collapse-only""expand-and-collapse"No

See Snap Points for transition-aware scrollables and the complete handoff behavior.

Per-Direction Activation Area

TSX

1options={{
2 gestureEnabled: true,
3 gestureDirection: { gesture: "horizontal", area: "edge" },
4}}

Configure each active pan direction independently:

TSX

1options={{
2 gestureEnabled: true,
3 gestureDirection: [
4 { gesture: "horizontal", area: "edge" },
5 { gesture: "horizontal-inverted", area: "screen" },
6 { gesture: "vertical", area: 32 },
7 ],
8}}

"edge" uses the default edge hit area. A number uses that many points from the edge.

Release Tuning

These options control how release velocity affects the outcome and the spring feel:

TSX

1options={{
2 gestureEnabled: true,
3 gestureVelocityImpact: 0.3,
4 gestureSnapVelocityImpact: 0.1,
5 gestureReleaseVelocityScale: 1.5,
6}}
  • use gestureVelocityImpact to change whether a fling dismisses
  • use gestureSnapVelocityImpact to change which snap point a sheet settles to
  • use gestureReleaseVelocityScale to change the release energy used by gesture reset and handoff values

Pinch Gestures

Use pinch-in or pinch-out when two-finger scale should drive dismissal:

TSX

1<Stack.Screen
2 name="Photo"
3 component={PhotoScreen}
4 options={{
5 gestureEnabled: true,
6 gestureDirection: ["pinch-in", "horizontal", "vertical"],
7 screenStyleInterpolator: ({ active, progress }) => {
8 "worklet";
9
10 return {
11 content: {
12 style: {
13 transform: [{ scale: 1 - Math.abs(active.gesture.normScale) * 0.2 }],
14 },
15 },
16 };
17 },
18 }}
19/>

active.gesture.scale, active.gesture.normScale, active.gesture.focalX, and active.gesture.focalY are available inside the interpolator. Use active.gesture.raw.normScale when computing dynamic gestureSensitivity, so the sensitivity calculation does not feed back into itself.

Pan, pinch, and rotation run as a simultaneous gesture composition when the screen config allows them. The gesture that is currently active owns navigation release, while the other gestures can still update live values for animation.

Rotation Values

Screens with pinch-in or pinch-out also track two-finger rotation.

TSX

1screenStyleInterpolator: ({ current }) => {
2 "worklet";
3
4 return {
5 content: {
6 style: {
7 transform: [{ rotateZ: `${current.gesture.rotation}rad` }],
8 },
9 },
10 };
11};

Use current.gesture.raw.rotation when you need the physical rotation before gestureSensitivity is applied.

Progress During Gestures

progress always includes live gesture movement. This is the value you want when the screen should follow the user's finger.

Use current.transitionProgress when animation logic needs transition or snap progress without the active gesture:

TSX

1screenStyleInterpolator: ({ current }) => {
2 "worklet";
3
4 return {
5 backdrop: {
6 style: {
7 opacity: current.transitionProgress,
8 },
9 },
10 };
11};

Dynamic Gesture Options

React-side option changes can be applied while the screen stays mounted:

TSX

1const navigation = useNavigation();
2
3navigation.setOptions({
4 gestureEnabled: false,
5 gestureDirection: "pinch-in",
6});

For per-frame changes from a worklet, return runtime options from the interpolator:

TSX

1screenStyleInterpolator: ({ active }) => {
2 "worklet";
3
4 return {
5 options: {
6 gestureSensitivity: interpolate(
7 Math.abs(active.gesture.raw.normY),
8 [0, 0.25],
9 [1, 0.5],
10 "clamp"
11 ),
12 },
13 };
14};

active.options exposes the resolved option state back to the interpolator, so animation logic can react to values changed by navigation.setOptions() or by previous runtime overrides.

Gesture Tracking

Use gestureTracking when a screen needs explicit control over live gesture values:

TSX

1options={{
2 gestureEnabled: false,
3 gestureTracking: "always",
4}}

"auto" tracks gestures when dismissal is enabled, or when snap points can move without dismissal. "always" keeps tracking while gestureEnabled is false, which is useful for resistance or shadowing effects where the screen should visually respond to a gesture but must not dismiss. "never" disables tracking for the screen, including snap gestures.

Snap Sheets

Snap sheets are regular screens with snapPoints:

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 initialSnapIndex: 0,
9 }}
10/>

Two behaviors matter here:

  • gestureEnabled: false disables dismiss at the minimum detent, but the screen can still snap between non-dismiss detents
  • gestureSnapLocked locks gesture-driven snap movement to the current detent, while programmatic changes such as navigation.setOptions() or snapTo() can still change behavior
  • sheetSnapBehavior: "step" makes each gesture operate only between its starting detent and one adjacent detent

Use step behavior when nearby snap points should still require a deliberate drag. For example, the 0.9 to 1 interval below receives the same normalized drag range as any other adjacent interval:

TSX

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

If you want to toggle snap locking at runtime:

TSX

1const navigation = useNavigation();
2
3navigation.setOptions({
4 gestureSnapLocked: true,
5});

Reading Gesture State

Gesture values are exposed inside the interpolator on current.gesture:

TSX

1screenStyleInterpolator: ({ current }) => {
2 "worklet";
3
4 return {
5 content: {
6 style: {
7 transform: [
8 { translateX: current.gesture.x },
9 { translateY: current.gesture.y },
10 ],
11 },
12 },
13 };
14};

Useful values include:

  • x, y
  • normX, normY
  • scale, normScale
  • focalX, focalY
  • pinchOriginX, pinchOriginY
  • rotation
  • velocity
  • raw
  • initiator
  • handoff
  • dragging
  • dismissing
  • settling

Live gesture values reset after release. If a dismiss animation needs the last release-time values while the live fields reset, read current.gesture.handoff.

focalX and focalY are live screen coordinates and can move with the fingers. pinchOriginX and pinchOriginY capture the screen-coordinate focal point at activation, which is usually the correct pivot for stable pinch-and-rotate transforms.

Hook Targeting

TSX

1const selfGestureRef = useScreenGesture();
2const parentGestureRef = useScreenGesture({ depth: -1 });
3const grandparentGestureRef = useScreenGesture({ depth: -2 });
4
5const parentAnimation = useScreenAnimation({ depth: -1 });
6const childAnimation = useScreenAnimation({ depth: 1 });

useScreenGesture() returns a navigator pan-gesture ref for Gesture Handler relations:

TSX

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

useScreenGesture() supports depth: 0 for the current screen and negative depths for ancestors. useScreenAnimation() also supports positive depths for descendant transition scopes. Ancestor targeting stops at navigation-host isolation boundaries, where negative depths resolve to null.