# How to Implement Swipe-to-Dismiss with Velocity Detection in React

> Implement swipe-to-dismiss with velocity detection in React. Learn to dismiss elements based on drag distance or pointer release velocity for a smooth user experience.

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

---

**A swipe-to-dismiss interaction requires calculating velocity on pointer release and dismissing the element if either the drag distance exceeds a threshold (typically 80 px) or the velocity exceeds 0.11 px/ms.**

Implementing tactile, native-feeling swipe gestures in web applications demands more than simple distance tracking. According to the `emilkowalski/skills` repository design guidelines, effective swipe-to-dismiss logic must account for both deliberate drags and quick flicks. The approach documented in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) establishes a velocity-based dismissal pattern that mirrors native mobile physics, ensuring users can efficiently clear items with a single gesture regardless of travel distance.

## Core Principles of Velocity-Based Dismissal

The repository outlines a six-step pattern for implementing momentum-aware dismissal. This approach separates the concern of gesture distance from gesture speed, creating a responsive interface that adapts to user intent.

### Capturing Initial Gesture Data

On `pointerdown` (or `touchstart`), record the initial **X coordinate** and **timestamp**. This baseline allows the system to calculate displacement and duration on release. The implementation in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) emphasizes using **pointer capture** to ensure the element continues receiving events even if the user's finger moves outside the component bounds during a rapid swipe.

### Tracking Movement and Calculating Velocity

During the `pointermove` phase, update the element's transform position to follow the finger. When the gesture ends on `pointerup`, compute velocity using the formula:

```

velocity = |distance| / elapsedTime

```

The repository specifies a **velocity threshold of approximately 0.11 px/ms** as the cutoff for dismissal. This means a user can swipe just 55 pixels in 500 milliseconds and still trigger dismissal because the speed indicates clear intent to dismiss.

### Combining Distance and Velocity Thresholds

The dismissal decision logic follows an **OR** condition:

- **Distance threshold**: The user drags the element at least `SWIPE_THRESHOLD` pixels (typically 80 px)
- **Velocity threshold**: The calculated velocity exceeds 0.11 px/ms

As implemented in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) (lines 441-450), this dual-threshold system accommodates both slow, deliberate drags and quick flicks that barely move the element before release.

## React Implementation with Framer Motion

Below is a production-ready React hook based on the repository's guidelines. It uses Framer Motion for hardware-accelerated animations and pointer events for broad device compatibility.

```typescript
import { useRef, useCallback } from "react";
import { motion, useAnimation } from "framer-motion";

const SWIPE_THRESHOLD = 80; // px
const VELOCITY_THRESHOLD = 0.11; // px/ms

export function useSwipeToDismiss(onDismiss: () => void) {
  const startX = useRef(0);
  const startTime = useRef(0);
  const controls = useAnimation();

  const onPointerDown = useCallback((e: React.PointerEvent) => {
    e.currentTarget.setPointerCapture(e.pointerId);
    startX.current = e.clientX;
    startTime.current = Date.now();
  }, []);

  const onPointerMove = useCallback((e: React.PointerEvent) => {
    const delta = e.clientX - startX.current;
    controls.set({ x: delta });
  }, [controls]);

  const onPointerUp = useCallback(
    (e: React.PointerEvent) => {
      const delta = e.clientX - startX.current;
      const elapsed = Date.now() - startTime.current;
      const velocity = Math.abs(delta) / elapsed;

      const shouldDismiss =
        Math.abs(delta) >= SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD;

      if (shouldDismiss) {
        controls.start({
          x: delta > 0 ? 1000 : -1000,
          transition: { type: "spring", bounce: 0.2, stiffness: 300 },
        }).then(onDismiss);
      } else {
        controls.start({ x: 0, transition: { type: "spring", bounce: 0 } });
      }

      e.currentTarget.releasePointerCapture(e.pointerId);
    },
    [controls, onDismiss]
  );

  return {
    controls,
    handlers: {
      onPointerDown,
      onPointerMove,
      onPointerUp,
    },
  };
}

```

### Usage Example

Apply the hook to any dismissible component:

```tsx
import { motion } from "framer-motion";
import { useSwipeToDismiss } from "./useSwipeToDismiss";

function NotificationCard({ message, onClose }) {
  const { controls, handlers } = useSwipeToDismiss(onClose);

  return (
    <motion.div
      animate={controls}
      {...handlers}
      className="notification"
      style={{ touchAction: "pan-y" }}
    >
      {message}
    </motion.div>
  );
}

```

The `touchAction: "pan-y"` CSS property ensures vertical scrolling remains functional while horizontal swipes control dismissal.

## Handling Critical Edge Cases

Robust swipe-to-dismiss implementations must address several interaction failures that degrade user experience.

### Hysteresis and Gesture Recognition

To prevent accidental dismissals during scrolling or tapping, implement a **hysteresis threshold** of approximately 10 pixels. Ignore movements below this threshold until the user clearly intends to swipe. This prevents jitter when the user initially touches the screen with slight lateral drift.

### Pointer Capture for Reliability

As noted in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) (lines 58-62), **pointer capture** is essential. Without `setPointerCapture`, a rapid swipe that exits the element's bounds before `pointerup` fires will lose the event listener, leaving the element stuck in a half-dragged state. Always capture the pointer on `pointerdown` and release it on `pointerup`.

### Multi-Touch Protection

After initiating a drag with the first touch point, ignore additional `pointerdown` events. Multiple simultaneous touch points can cause the `startX` reference to jump erratically, breaking the velocity calculation and creating jarring visual glitches.

### Spring-Based Dismissal Animations

When dismissing, use **spring physics** rather than linear easing. Springs preserve the gesture's momentum and allow interruption if the user starts another interaction. The repository recommends springs with `stiffness: 300` and `bounce: 0.2` for the exit animation, and `bounce: 0` for the snap-back animation when dismissal criteria aren't met.

## Summary

- **Calculate velocity** on every gesture end using `|distance| / elapsedTime` and compare against the 0.11 px/ms threshold defined in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)
- **Combine thresholds** so dismissal triggers if either distance exceeds 80 px OR velocity exceeds 0.11 px/ms
- **Use pointer capture** to maintain event tracking when swipes exit element bounds
- **Animate with springs** to preserve momentum and enable gesture interruption
- **Implement hysteresis** (10 px minimum) to distinguish swipes from taps and scrolls

## Frequently Asked Questions

### What is the optimal velocity threshold for swipe-to-dismiss?

The `emilkowalski/skills` repository specifies **0.11 px/ms** as the optimal velocity threshold for swipe-to-dismiss interactions. This value strikes a balance between sensitivity and precision, allowing quick flicks to dismiss items while preventing accidental dismissals during normal scrolling or tapping. Calculated as `absolute distance divided by elapsed time`, this threshold means a 100-pixel swipe completed in 909 milliseconds will trigger dismissal on velocity alone.

### Should I use distance or velocity to determine dismissal?

You should use **both**. Relying solely on distance forces users to drag items completely across the screen, while velocity-only detection can trigger accidentally during fast scrolling. The recommended pattern checks if **either** the drag distance exceeds a static threshold (typically 80 px) **or** the velocity exceeds 0.11 px/ms. This dual-threshold approach accommodates both deliberate dragging and quick flicking gestures.

### Why does my swipe-to-dismiss stop working when I drag too fast?

This typically occurs due to missing **pointer capture**. When a user swipes rapidly, their finger often leaves the element's bounding box before the `pointerup` event fires. Without `setPointerCapture(e.pointerId)` called during `pointerdown`, the browser stops sending events to your handler once the pointer exits the element. Always capture the pointer on gesture start and release it on end, as demonstrated in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) (lines 58-62).

### How do I prevent swipe-to-dismiss from interfering with vertical scrolling?

Apply the CSS property `touch-action: pan-y` to the swipeable element. This declaration tells the browser to handle vertical scrolling natively while allowing your JavaScript to process horizontal pointer events. Without this setting, the browser may interpret horizontal swipes as scroll attempts or, conversely, block vertical scrolling when the user touches a swipeable card.