How to Implement Momentum-Based Swipe Dismissal with Velocity

To implement momentum-based swipe dismissal, calculate velocity by dividing the absolute drag distance by elapsed time in milliseconds, then trigger dismissal when velocity exceeds 0.11 px/ms or a safety distance threshold is crossed.

The emilkowalski/skills repository defines specific standards for gesture-based interactions in the Emil Design Engineering skill files. Unlike fixed-distance swipe detection, momentum-based dismissal measures the speed of the user's gesture to create a physics-inspired throwing motion that feels natural across different screen sizes.

The Velocity Formula

The core calculation for momentum-based dismissal is straightforward but requires precise timing. According to the implementation details in skills/emil-design-eng/SKILL.md (lines 441-452), velocity is calculated as:

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

This formula measures how many pixels the user moved per millisecond. The dragDistance represents the total horizontal or vertical displacement from the gesture start, while elapsedMs captures the total time between pointer down and pointer up events.

Step-by-Step Implementation

Record Start Position and Time

When the user begins the drag gesture, capture both the initial coordinate and a timestamp. This establishes the baseline for all subsequent calculations.

const handlePointerDown = (e) => {
  startX.current = e.clientX;
  startTime.current = Date.now();
};

Track Movement Interactively

While the user drags, update the visual position of the element in real-time. However, the critical velocity calculation happens only at the end of the gesture when the pointer releases.

Calculate Velocity on Release

When the drag ends, compute the total distance traveled and the elapsed duration. The absolute value ensures direction-agnostic speed measurement.

const handlePointerUp = (e) => {
  const distance = e.clientX - startX.current;
  const elapsed = Date.now() - startTime.current;
  const velocity = Math.abs(distance) / elapsed;
  
  // Continue with threshold check...
};

Apply Threshold Logic

The dismissal logic combines velocity with an optional distance fallback for robustness. As documented in the standards, use a velocity threshold of 0.11 px/ms (pixels per millisecond). You may also include a secondary distance threshold (typically 100px) to ensure slow but deliberate drags still dismiss.

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

if (Math.abs(distance) >= SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
  onDismiss(); // Trigger dismissal animation
}

React Implementation Examples

Minimal Hook Approach

This standalone React hook implements the momentum calculation without external animation libraries:

import { useRef } from "react";

function SwipeDismiss({ onDismiss }) {
  const startX = useRef(0);
  const startTime = useRef(0);
  const SWIPE_THRESHOLD = 100;
  const VELOCITY_THRESHOLD = 0.11;

  const handlePointerDown = (e) => {
    startX.current = e.clientX;
    startTime.current = Date.now();
  };

  const handlePointerUp = (e) => {
    const distance = e.clientX - startX.current;
    const elapsed = Date.now() - startTime.current;
    const velocity = Math.abs(distance) / elapsed;

    if (Math.abs(distance) >= SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
      onDismiss();
    }
  };

  return (
    <div
      onPointerDown={handlePointerDown}
      onPointerUp={handlePointerUp}
      style={{ touchAction: "none", userSelect: "none" }}
    >
      {/* swipe-able content */}
    </div>
  );
}

Framer Motion with Springs

For applications using Framer Motion, combine velocity detection with spring physics to create interruptible animations that preserve momentum. The skills/review-animations/STANDARDS.md (lines 61-73) specifically recommends springs for gesture-based dismissal because they naturally handle velocity carryover when interrupted.

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

export default function Swipeable({ onDismiss }) {
  const startX = useRef(0);
  const startTime = useRef(0);
  const VELOCITY_THRESHOLD = 0.11;

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

  const handleDragStart = (info) => {
    startX.current = info.point.x;
    startTime.current = Date.now();
  };

  const handleDragEnd = (info) => {
    const distance = info.point.x - startX.current;
    const elapsed = Date.now() - startTime.current;
    const velocity = Math.abs(distance) / elapsed;

    if (velocity > VELOCITY_THRESHOLD) {
      onDismiss();
    } else {
      // Snap back with spring physics
      spring.set(0);
    }
  };

  return (
    <motion.div
      drag="x"
      onDragStart={(_, info) => handleDragStart(info)}
      onDragEnd={(_, info) => handleDragEnd(info)}
      style={{ x: spring }}
    >
      {/* content */}
    </motion.div>
  );
}

Why Springs Matter for Momentum

The animation standards in skills/review-animations/STANDARDS.md (lines 37-44) mandate spring-based animations for momentum gestures rather than ease-out curves. Springs preserve velocity when interrupted, meaning if the user grabs the element mid-animation, the motion continues naturally from the current speed rather than resetting.

This physicality creates a consistent experience where quick flicks dismiss elements regardless of screen size, while slow drags require crossing the distance threshold. The bounce and damping parameters (typically bounce: 0.2 to 0.3) provide the tactile resistance that users expect from real-world objects.

Summary

Frequently Asked Questions

What is the exact velocity threshold for dismissal?

According to the emilkowalski/skills repository documentation, the dismissal threshold is 0.11 pixels per millisecond (px/ms). This value is derived from empirical testing to distinguish between casual scrolling and intentional throw gestures.

Should I use velocity alone or combine it with distance checks?

While velocity is the primary metric for momentum-based dismissal, the standards recommend combining it with an optional distance threshold (typically 100px). This ensures that slow, deliberate drags across sufficient distance still trigger dismissal even if they don't generate high velocity.

Why are springs preferred over ease-out animations for swipe dismissal?

Springs are preferred because they preserve the element's velocity when interrupted. If a user catches the element mid-flight, the spring continues from that velocity naturally, whereas ease-out animations would create a discontinuity. This behavior is documented in skills/review-animations/STANDARDS.md under the Physicality and Springs section.

How do I handle direction-specific dismissal (left vs right)?

Check the sign of dragDistance before taking the absolute value for velocity calculation. A positive distance indicates rightward movement, while negative indicates leftward. Apply the velocity threshold to the absolute value, then use the original signed distance to determine the dismissal direction and animation target.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →