Gesture & drag
Teaching your UI to understand flicks, swipes, and drags
Drag requires frame-by-frame response: zero perceptible lag, resistance at boundaries, natural settle on release.
## Pointer capture
Without `setPointerCapture`, the element loses track of the cursor the moment it escapes bounds.
```tsx
const handlePointerDown = (e: React.PointerEvent) => {
(e.target as HTMLElement).setPointerCapture(e.pointerId);
// Begin tracking
};
```
Always use `pointer` events, not `mouse` events. Pointer events unify mouse, touch, and stylus.
}
/>
## Momentum
Track the last 2-3 pointer positions with timestamps. Compute velocity as `deltaPosition / deltaTime`. Pass that velocity to a spring.
```tsx
// On release, pass the gesture velocity to the spring
animate(x, snapPoint, {
type: "spring",
velocity: gesture.velocity,
stiffness: 400,
damping: 40,
});
```
Motion's `drag` prop handles this automatically.
}
/>
## Boundary damping
Apply logarithmic damping past the boundary:
```tsx
function rubberBand(offset: number, dimension: number, constant = 0.55) {
return (offset * dimension * constant) / (dimension + constant * offset);
}
```
0.55 is iOS's value. 0.3 feels stiffer. 0.8 feels loose.
## Swipe-to-dismiss
Use both distance and velocity, never just one.
```tsx
const shouldDismiss =
Math.abs(offset) > dimension * 0.35 || Math.abs(velocity) > 500;
```
If neither is met, spring back. On dismiss, animate off-screen with current velocity.
## Touch-action and scroll conflict
```css
/* Horizontal drag: let the browser handle vertical scroll */
.horizontal-drag {
touch-action: pan-y;
}
/* Vertical drag: let the browser handle horizontal scroll */
.vertical-drag {
touch-action: pan-x;
}
/* Full custom gesture: prevent all browser handling */
.custom-gesture {
touch-action: none;
}
```
## Axis locking
On the first `pointermove`, lock to whichever axis has the larger delta.
```tsx
if (!axisLock) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
axisLock = "x";
} else {
axisLock = "y";
}
}
```
Apply a 5-8px dead zone before checking.
## The 60fps requirement
- **Only animate `transform` and `opacity`.**
- **Use `will-change: transform`** on the dragged element. Remove it when drag ends.
- **Avoid React re-renders during drag.** Use `useMotionValue` or refs, not state.