# How to Implement Gesture-Based Drag Dismissal with Momentum in React

> Implement gesture-based drag dismissal with momentum in React. Learn to use pointer capture for velocity tracking and apply spring animations that preserve momentum on release.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-08-01

---

**Use pointer capture to track drag velocity, then apply a spring animation that preserves momentum when the release velocity exceeds 0.11 px/ms.**

Gesture-based drag dismissal with momentum creates fluid, physics-based interactions where dragged elements continue moving naturally after release. The `emilkowalski/skills` repository documents this pattern extensively in its design-engineering skill files, providing concrete thresholds and implementation strategies used in production-grade interfaces.

## Core Architecture for Momentum Drag Dismissal

The repository outlines six essential steps in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md):

### 1. Capture the Drag with Pointer Events

Pointer capture ensures the gesture continues even when the cursor leaves the element's bounds. Call `setPointerCapture()` on `pointerdown` and store the initial state.

### 2. Track Start Time and Distance

Record the timestamp when dragging begins and accumulate the total drag distance throughout the gesture.

### 3. Calculate Velocity on Release

Compute velocity as `|dragDistance| / elapsedTime`. According to the skill file, the dismissal threshold is **0.11 px/ms**—if velocity exceeds this value, dismiss regardless of distance traveled.

### 4. Apply a Spring Animation

Springs outperform fixed-duration CSS transitions because they preserve release velocity and support interruption. The recommended configuration from the source:

```javascript
{
  type: "spring",
  duration: 0.5,
  bounce: 0.2
}

```

### 5. Implement Damping at Bounds

When users drag past natural edges (e.g., pulling a drawer already at top), apply progressive friction rather than hard stops. This "rubber-band" effect is documented in [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) at lines 140-143.

### 6. Ensure Interruptibility

Spring-based animations allow instant retargeting if the user reverses direction mid-dismissal.

## React Implementation with Framer Motion

This complete hook implements the velocity-threshold logic from the design-engineering skill:

```tsx
import { useRef, useState } from "react";
import { motion, useSpring } from "framer-motion";

const DRAG_THRESHOLD = 30; // px
const VELOCITY_THRESHOLD = 0.11; // px/ms

export function DragDismiss({ children }) {
  const dragStart = useRef<Date | null>(null);
  const dragDelta = useRef(0);
  const [dismissed, setDismissed] = useState(false);

  const spring = useSpring(0, {
    type: "spring",
    duration: 0.5,
    bounce: 0.2,
  });

  const onPointerDown = (e: React.PointerEvent) => {
    e.currentTarget.setPointerCapture(e.pointerId);
    dragStart.current = new Date();
    dragDelta.current = 0;
  };

  const onPointerMove = (e: React.PointerEvent) => {
    if (!dragStart.current) return;
    dragDelta.current = e.movementX;
    spring.set(dragDelta.current);
  };

  const onPointerUp = (e: React.PointerEvent) => {
    if (!dragStart.current) return;
    const elapsed = new Date().getTime() - dragStart.current.getTime();
    const velocity = Math.abs(dragDelta.current) / elapsed;

    const shouldDismiss =
      Math.abs(dragDelta.current) > DRAG_THRESHOLD || velocity > VELOCITY_THRESHOLD;

    if (shouldDismiss) {
      setDismissed(true);
    } else {
      spring.set(0); // Snap back
    }

    dragStart.current = null;
  };

  if (dismissed) return null;

  return (
    <motion.div
      style={{ x: spring }}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
    >
      {children}
    </motion.div>
  );
}

```

Key implementation details from the source:

- `setPointerCapture()` guarantees uninterrupted dragging outside element bounds
- The 0.11 px/ms velocity threshold matches the specification in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) lines 44-52
- The spring retains release velocity for natural momentum

## Vanilla JavaScript Alternative

For projects without React, implement the same physics using the Web Animations API with Apple's recommended easing curve:

```html
<div class="dismissable" id="dragTarget">
  Drag me
</div>

<script>
const el = document.getElementById("dragTarget");
let startX = 0;
let startTime = 0;

el.addEventListener("pointerdown", e => {
  el.setPointerCapture(e.pointerId);
  startX = e.clientX;
  startTime = performance.now();
});

el.addEventListener("pointermove", e => {
  if (e.pressure === 0) return;
  const delta = e.clientX - startX;
  el.style.setProperty("--drag-x", `${delta}px`);
});

el.addEventListener("pointerup", e => {
  const elapsed = performance.now() - startTime;
  const delta = e.clientX - startX;
  const velocity = Math.abs(delta) / elapsed;
  const shouldDismiss = Math.abs(delta) > 30 || velocity > 0.11;

  if (shouldDismiss) {
    el.animate(
      [{ transform: `translateX(${delta}px)` }, { transform: "translateX(100vw)" }],
      {
        duration: 500,
        easing: "cubic-bezier(0.23, 1, 0.32, 1)", // Apple-style spring
        fill: "forwards",
      }
    );
  } else {
    el.animate(
      [{ transform: `translateX(${delta}px)` }, { transform: "translateX(0)" }],
      { duration: 300, easing: "cubic-bezier(0.32, 0.72, 0, 1)", fill: "forwards" }
    );
  }
});
</script>

<style>
.dismissable {
  --drag-x: 0px;
  transform: translateX(var(--drag-x));
  transition: transform 0.2s ease-out;
  touch-action: none;
}
</style>

```

The `cubic-bezier(0.23, 1, 0.32, 1)` curve corresponds to "Apple's approach (recommended)" documented in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) lines 81-84.

## Spring vs. Fixed-Duration: Why Physics Matters

| Approach | Interruptibility | Momentum Preservation | Recommended Use |
|----------|---------------|----------------------|---------------|
| **CSS transitions** | No—must complete or jump | No—fixed duration | Simple state changes |
| **Spring physics** | Yes—retargets instantly | Yes—carries velocity | Drag gestures, dismissals |

The [`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md) file specifically warns against fixed-duration keyframes for gesture-based interactions, noting that springs prevent the "jarring interruption" common in poorly implemented dismissals.

## Handling Edge Cases

### Vertical Dismissal

Replace `movementX` with `movementY` and adjust the exit transform:

```tsx
dragDelta.current = e.movementY;
// In dismissal animation:
spring.set(window.innerHeight); // Animate off-screen downward

```

### Bidirectional Dragging

Track both axes independently and dismiss toward the dominant direction:

```tsx
const velocityX = Math.abs(e.movementX) / elapsed;
const velocityY = Math.abs(e.movementY) / elapsed;
const dominant = velocityX > velocityY ? "x" : "y";

```

### Accessibility Considerations

Add keyboard dismissal as a fallback—momentum physics should not block users who cannot perform drag gestures.

## Summary

- **Pointer capture** (`setPointerCapture`) enables reliable drag tracking across element boundaries
- **Velocity threshold of 0.11 px/ms** determines dismissal eligibility regardless of distance
- **Spring animations** with `{ duration: 0.5, bounce: 0.2 }` preserve momentum and allow interruption
- **Damping at bounds** creates physical rubber-band resistance when over-dragging
- **Apple-style easing curves** (`cubic-bezier(0.23, 1, 0.32, 1)`) match platform-native feel

## Frequently Asked Questions

### What is the minimum velocity required to trigger momentum dismissal?

The `emilkowalski/skills` repository specifies **0.11 pixels per millisecond** as the velocity threshold. This value appears in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) as the standard for determining whether a quick flick should dismiss an element even if the total drag distance is small.

### Why use springs instead of CSS transitions for drag dismissal?

Springs maintain the velocity from the user's release gesture and can be retargeted mid-animation if the user reverses direction. CSS transitions run on fixed durations and cannot preserve momentum or respond to interruption. The repository's animation standards in [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) explicitly recommend springs for all gesture-based interactions.

### How do I prevent drag dismissal from feeling too sensitive?

Increase the `DRAG_THRESHOLD` constant for distance-based gating, or reduce the velocity threshold below 0.11 px/ms. You can also add a time minimum—require at least 100ms of drag time before evaluating velocity to filter out accidental taps.