# Momentum-Based Swipe Dismissal with Velocity Calculations: A Complete Implementation Guide

> Implement momentum-based swipe dismissal with velocity calculations. This guide explains how physics-inspired interactions create natural swipe dismissals for your app.

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

---

**Momentum-based swipe dismissal uses drag velocity (pixels per millisecond) rather than fixed distance thresholds to determine whether to dismiss an element, creating natural, physics-inspired interactions.**

This guide walks through implementing velocity-based swipe gestures in JavaScript and React, based on the animation standards and design engineering principles from the `emilkowalski/skills` repository. The approach combines precise velocity calculations with spring animations for interruptible, responsive UI patterns.

## Core Algorithm: Recording Velocity for Swipe Dismissal

The velocity calculation follows a three-phase pattern documented in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) (lines 441–452). Understanding this flow is essential before implementation.

### Phase 1: Capture Gesture Start

Record the initial pointer position and timestamp when the user begins dragging. Store these as references to avoid re-renders and ensure accurate timing.

### Phase 2: Measure on Release

When the drag ends, compute:

```javascript
const velocity = Math.abs(dragDistance) / elapsedMs; // px / ms

```

The `dragDistance` is the total horizontal (or vertical) displacement from start to end. The `elapsedMs` is `Date.now() - startTime`. The absolute value ensures velocity is always positive regardless of swipe direction.

### Phase 3: Apply Threshold Logic

Compare against the standard threshold:

```javascript
const VELOCITY_THRESHOLD = 0.11; // px / ms

if (velocity > VELOCITY_THRESHOLD) {
  // Dismiss with momentum
}

```

As implemented in the repository, `0.11 px/ms` translates to roughly 110 pixels per second—fast enough to feel intentional, slow enough to avoid accidental triggers.

## Why Springs Are Required for Momentum Gestures

The [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) file (lines 61–73) mandates **spring-based animations** for all momentum interactions. Unlike duration-based transitions, springs preserve velocity when interrupted.

When a user flicks an element, then grabs it mid-flight, the spring continues from the current velocity rather than resetting. This creates the "physical" feel that distinguishes polished interfaces.

Key spring parameters from the standards:
- **Damping**: Higher values reduce oscillation (recommended: 25–40)
- **Stiffness**: Controls responsiveness (recommended: 300–500)
- **Mass**: Virtual weight of the element (typically 1)

## Minimal React Implementation

This hook-based approach works with any rendering layer—no animation library required.

```jsx
import { useRef, useCallback } from "react";

function useMomentumDismiss({ onDismiss, direction = "x" }) {
  const startPos = useRef(0);
  const startTime = useRef(0);
  
  // Thresholds from emilkowalski/skills standards
  const VELOCITY_THRESHOLD = 0.11; // px / ms
  const DISTANCE_FALLBACK = 100;   // px, for accessibility

  const handlePointerDown = useCallback((e) => {
    const point = direction === "x" ? e.clientX : e.clientY;
    startPos.current = point;
    startTime.current = Date.now();
    
    // Capture pointer for reliable drag tracking
    e.currentTarget.setPointerCapture(e.pointerId);
  }, [direction]);

  const handlePointerUp = useCallback((e) => {
    const point = direction === "x" ? e.clientX : e.clientY;
    const distance = point - startPos.current;
    const elapsed = Date.now() - startTime.current;
    
    // Avoid division by zero on instantaneous taps
    if (elapsed === 0) return;
    
    const velocity = Math.abs(distance) / elapsed;

    // Velocity-primary check with distance fallback
    const shouldDismiss = velocity > VELOCITY_THRESHOLD || 
                          Math.abs(distance) >= DISTANCE_FALLBACK;

    if (shouldDismiss) {
      // Pass velocity and direction for animation continuity
      onDismiss({
        velocity,
        direction: distance > 0 ? 1 : -1,
        distance
      });
    }
  }, [direction, onDismiss]);

  return {
    onPointerDown: handlePointerDown,
    onPointerUp: handlePointerUp,
    // Prevent scrolling while swiping
    style: { touchAction: "none", userSelect: "none" }
  };
}

// Usage
function SwipeCard({ children, onRemove }) {
  const dismissProps = useMomentumDismiss({
    onDismiss: ({ velocity, direction }) => {
      // Animate out using velocity-preserved spring
      console.log(`Dismissing at ${velocity.toFixed(3)} px/ms`);
      onRemove();
    }
  });

  return (
    <div {...dismissProps}>
      {children}
    </div>
  );
}

```

The `touchAction: "none"` CSS property is critical—it prevents the browser's default touch behaviors (scrolling, zooming) while maintaining accessibility via pointer events.

## Framer Motion Integration with Velocity Continuity

For production React applications, Framer Motion's `useMotionValue` and `useSpring` provide hardware-accelerated animations with automatic velocity preservation. This implementation mirrors the gesture standards in [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) (lines 37–44).

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

interface SwipeableProps {
  onDismiss: (direction: number) => void;
  children: React.ReactNode;
}

export function Swipeable({ onDismiss, children }: SwipeableProps) {
  const x = useMotionValue(0);
  const startTime = useRef(0);
  
  // Spring configuration from animation standards
  const spring = useSpring(x, {
    stiffness: 400,
    damping: 30,
    mass: 1
  });

  // Visual feedback: reduce opacity as card moves away
  const opacity = useTransform(
    spring,
    [-300, 0, 300],
    [0.2, 1, 0.2]
  );

  const handleDragStart = () => {
    startTime.current = Date.now();
  };

  const handleDragEnd = (_: any, info: { offset: { x: number }; velocity: { x: number } }) => {
    const elapsed = Date.now() - startTime.current;
    const distance = info.offset.x;
    
    // Calculate velocity manually for consistency with standards
    const calculatedVelocity = Math.abs(distance) / elapsed;
    const VELOCITY_THRESHOLD = 0.11;
    
    // Combine calculated velocity with Framer's instantaneous velocity
    const effectiveVelocity = Math.max(
      calculatedVelocity,
      Math.abs(info.velocity.x) / 1000 // Framer uses px/s, convert to px/ms
    );

    if (effectiveVelocity > VELOCITY_THRESHOLD || Math.abs(distance) > 150) {
      // Animate to dismissal with momentum
      const dismissDirection = distance > 0 ? 1 : -1;
      spring.set(dismissDirection * 500); // Off-screen
      
      setTimeout(() => onDismiss(dismissDirection), 200);
    } else {
      // Spring back to center
      spring.set(0);
    }
  };

  return (
    <motion.div
      style={{ x: spring, opacity }}
      drag="x"
      dragConstraints={{ left: 0, right: 0 }}
      dragElastic={0.2}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
    >
      {children}
    </motion.div>
  );
}

```

Notice the dual velocity calculation: Framer Motion provides instantaneous velocity via `info.velocity`, while the manual calculation matches the repository's standardized approach. Using the maximum of both ensures reliability across different gesture speeds.

## Handling Edge Cases and Accessibility

### Low-Velocity, Long-Distance Swipes

Some users drag slowly but deliberately. The distance fallback (`DISTANCE_FALLBACK = 100`) catches these interactions without requiring excessive velocity.

```javascript
const shouldDismiss = velocity > VELOCITY_THRESHOLD || 
                      Math.abs(distance) >= DISTANCE_FALLBACK;

```

### Direction-Agnostic Implementation

The same algorithm applies to vertical swipes—simply swap `clientX` for `clientY`:

```javascript
const isVertical = true;
const dragDistance = isVertical 
  ? e.clientY - startY.current 
  : e.clientX - startX.current;

```

### Reduced Motion Support

Respect `prefers-reduced-motion` by skipping the spring animation and using instant dismissal:

```javascript
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (prefersReducedMotion && shouldDismiss) {
  onDismiss(); // No animation
}

```

## Performance Considerations

Velocity calculation is lightweight—two `Date.now()` calls and a division operation. However, avoid storing motion values in React state during the drag, as this triggers re-renders every frame. Use `useRef` for start values and motion libraries for the animated values.

For maximum performance on low-end devices:
- Use `transform` and `opacity` exclusively (GPU-composited properties)
- Apply `will-change: transform` before the gesture begins
- Remove `will-change` after animation completes to free GPU memory

## Summary

- **Velocity formula**: `Math.abs(distance) / elapsedMs` yields pixels per millisecond, implemented in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)
- **Dismiss threshold**: `0.11 px/ms` is the standard cutoff for intentional flicks
- **Distance fallback**: Combine with `100px` minimum for accessibility and slow drags
- **Spring animations**: Required per [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) for interruptible, physics-based motion
- **Implementation pattern**: Record start position/time → calculate on release → compare to thresholds → animate with spring

## Frequently Asked Questions

### How do I convert velocity units for different animation libraries?

Most libraries use pixels per second (px/s) rather than px/ms. Multiply by 1000: `0.11 px/ms = 110 px/s`. Framer Motion's `info.velocity` already returns px/s, so divide by 1000 to compare against the repository's standard threshold.

### What if the user's device has a slow refresh rate?

Velocity calculation is time-based, not frame-based, so it remains accurate regardless of refresh rate. However, the perceived smoothness of the spring animation benefits from 60fps or higher. Consider reducing spring stiffness on low-power mode detection for consistent perceived performance.

### Can I use this approach with native mobile apps?

The algorithm translates directly to React Native using `Animated` or `react-native-reanimated`. The velocity threshold remains `0.11`, though you may adjust for typical mobile screen sizes. The [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) guidelines apply cross-platform—springs and velocity-based dismissal are framework-agnostic principles.