Core Concepts

Gestures

Configure dismiss gestures, snap-sheet handoff, backdrops, and gesture hooks from real 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

gestureActivationArea

Deprecated. Use gestureDirection entries with per-direction area instead.

typedefaultrequired
GestureActivationArea"screen"No

gestureResponseDistance

Deprecated. Use a numeric area on gestureDirection entries instead.

typedefaultrequired
numberundefinedNo

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

gestureProgressMode

Deprecated. Gesture movement now always contributes to progress. Use transitionProgress when an interpolator needs transition progress without live gesture movement.

typedefaultrequired
"progress-driven" | "freeform""progress-driven"No

gestureSnapVelocityImpact

Release velocity influence on snap-target selection.

typedefaultrequired
number0.1No

gestureSnapLocked

Locks gesture-driven snap movement to the current detent.

typedefaultrequired
booleanfalseNo

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

backdropBehavior

Controls how taps on the backdrop are handled.

typedefaultrequired
"block" | "passthrough" | "dismiss" | "collapse"stack defaultNo

backdropComponent

Custom backdrop component type. Pass a stable component such as BlurView, or pass a function component that renders { styles, props, pointerEvents }.

typedefaultrequired
React.ComponentType<any> | ScreenBackdropComponentundefinedNo

contentComponent

Custom renderer for the screen content layer. Use it when a screen needs a custom shell that receives the content slot's animated styles, animated props, pointer events, and children.

typedefaultrequired
React.ComponentType<any> | ScreenContentComponentundefinedNo

surfaceComponent

Deprecated custom surface wrapper. It still works for compatibility, but new custom shells should use contentComponent.

typedefaultrequired
React.ComponentType<any>undefinedNo

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.

gestureActivationArea still works for compatibility, but new code should use gestureDirection entries with area.

Gesture Response Distance

TSX

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

gestureResponseDistance is deprecated. Use a numeric area on the pan direction that needs a custom edge-start distance.

Despite the name, the old option controls how far from the edge a gesture may start when a direction uses "edge" activation. It does not change how far the pointer must move before pan activates; that movement threshold is internal.

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};

gestureProgressMode and gestureDrivesProgress remain as deprecated compatibility options. New code should choose between progress and transitionProgress inside the interpolator instead of configuring a screen-level progress mode.

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

If you want to toggle snap locking at runtime:

TSX

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

Scroll Handoff for Snap Sheets

When a snap sheet contains scroll content, use the transition-aware scrollables:

TSX

1function SheetScreen() {
2 return (
3 <Transition.ScrollView>
4 <LongContent />
5 </Transition.ScrollView>
6 );
7}

sheetScrollGestureBehavior controls what happens when that scroll view reaches its boundary:

expand-and-collapse

At the sheet boundary:

  • dragging toward expand can expand the sheet
  • dragging toward collapse can collapse or dismiss the sheet

This is the Apple Maps-style behavior.

collapse-only

At the sheet boundary:

  • dragging toward collapse can collapse or dismiss the sheet
  • dragging from the scrollable content does not expand the sheet

Expansion must start from dead space outside the scrollable content. This is the Instagram-style behavior.

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/>

Scroll Metadata

Transition-aware scrollables publish scroll state through current.layouts.scroll:

TSX

1screenStyleInterpolator: ({ current }) => {
2 "worklet";
3
4 const y = current.layouts.scroll?.vertical?.offset ?? 0;
5
6 return {
7 content: {
8 style: {
9 transform: [{ translateY: -Math.min(y, 24) }],
10 },
11 },
12 };
13};

For nested same-axis scrollables, the outermost scrollable owns that axis. Cross-axis nested scrollables can each publish their own axis.

Backdrop Interaction

The supported values are:

  • "block"
  • "passthrough"
  • "dismiss"
  • "collapse"

Use backdropBehavior: "dismiss" when tapping the backdrop should close the screen, or backdropBehavior: "collapse" when a snap sheet should step down to the next lower detent first.

For a custom backdrop renderer, pass a component type:

TSX

1import { BlurView } from "expo-blur";
2
3<Stack.Screen
4 name="Detail"
5 component={DetailScreen}
6 options={{
7 backdropBehavior: "dismiss",
8 backdropComponent: BlurView,
9 screenStyleInterpolator: ({ progress }) => {
10 "worklet";
11
12 return {
13 backdrop: {
14 style: {
15 opacity: interpolate(progress, [0, 1, 2], [0, 0.2, 0]),
16 },
17 props: {
18 intensity: interpolate(progress, [0, 1, 2], [0, 80, 0]),
19 },
20 },
21 };
22 },
23 }}
24/>

The library wraps the backdrop component with Animated.createAnimatedComponent internally and drives it from the backdrop slot.

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
  • handoff
  • dragging
  • dismissing
  • settling
  • active
  • direction (deprecated pan-only alias)

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 isolation boundaries. In an independent nested stack, negative depths can resolve to null when the lookup reaches that isolated tree edge.