Gestures

The two pans, the rubber band past the last snap point, the velocity snap, and the haptics that fire on each crossing.

Two Gesture.Pan()s move a sheet: one on the Handle, one on the Content. Both are built from the same handlers and both write the same value — the sheet's keyboard-free height — so a drag that starts on the pill and a drag that starts on the copy feel the same.

<BottomSheet
  enableContentPanningGesture
  enableHandlePanningGesture
  enableOverDrag
  enablePanDownToClose
  overDragResistanceFactor={2.5}
>

The handle pan

Claims a touch immediately. The handle is the surface for dragging, so there is nothing under it a tap could have meant.

<BottomSheet.Handle enablePanningGesture={false} />

A per-part opt-out leaves the detector out for that handle alone; the root's enableHandlePanningGesture switches every handle off.

The content pan

Waits six points of vertical travel before it claims a touch and gives up after twelve horizontal. A tap lands on the button or the field under it, and a horizontal pager inside the sheet keeps its swipe. Both pans keep running when the finger leaves the view.

A scrollable body wraps its own native scroll in a gesture declared simultaneous with this pan; Scrollables is the rule for which of the two a finger moves.

Over-drag

Past the highest snap point — and, when the sheet may not close, past the lowest — the sheet follows the finger with a rubber band: slope one at the edge, square root beyond. overDragResistanceFactor scales the travel; larger allows more. enableOverDrag={false} clamps instead.

The sheet never goes above the frame, however far the finger does.

Pan down to close

With enablePanDownToClose (the default) the closed position is a candidate for the release, and a drag below the lowest snap point runs toward it. Off, the lowest snap point is the floor and a drag below it rubber-bands back. A step marked dismissible: false switches it off for as long as that step is current.

The snap

A release picks the candidate nearest to where the finger was heading: the height it let go at plus a fifth of a second of its velocity. A quick flick lands a snap point away; a slow drag lands on the nearest one. Closed is a candidate only when the pan may close. On a tie the lower one wins.

The animation gets half the release velocity, in height space, so a spring continues the drag rather than starting from rest.

The snap runs on onFinalize, not onEnd: a gesture that fails after activating never fires onEnd, and the sheet would be left between snap points. If you compose a gesture of your own with the pans, read the finalize.

The keyboard, mid-drag

enableBlurKeyboardOnGesture (the default) dismisses the keyboard the moment a pan starts, and a pan that ends heading down dismisses it regardless. Both go through keyboard-controller's KeyboardController.dismiss on the JS thread.

Haptics

The package imports nothing from a haptics library. Three props take a worklet and run it on the UI thread, in the frame the crossing happened:

PropFires when
onSnapPointHaptica drag crossed a snap point
onCloseHaptica drag let go on a close
onOverDragHaptica drag left the snap point range and the rubber band took over — once per entry
import { Presets } from "react-native-pulsar";

<BottomSheet onCloseHaptic={Presets.System.impactLight} onSnapPointHaptic={Presets.System.selection}>

Pulsar's presets are worklets already; a function of your own needs the "worklet" directive and may call nothing that is not one.

Your own gesture

The pans are on the internal context as pans.handle and pans.content, ready for a GestureDetector each. They are one shared object per pan and a gesture builder mutates in place, so hand them to a detector untouched; a part that opts out leaves the detector out rather than calling .enabled(false) on the shared object, which would switch it off for every other part too.

On this page