How to Calculate Velocity for Momentum-Based Swipe Dismissal

Calculate velocity by dividing the absolute drag distance by the elapsed time in milliseconds, then trigger dismissal if the result exceeds 0.11 px/ms.

Momentum-based swipe dismissal creates natural, physics-driven interactions by measuring flick speed rather than relying on arbitrary distance thresholds. In the emilkowalski/skills repository, this pattern is codified in the design engineering standards to ensure consistent gesture handling across components. Mastering how to calculate velocity for momentum-based swipe dismissal allows you to implement interruptible animations that respond to user intent rather than fixed coordinates.

The Velocity Formula

According to skills/emil-design-eng/SKILL.md (lines 441-452), the core calculation relies on basic kinematics:

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

Where:

  • dragDistance is the total pixels moved from pointer start to end (positive or negative depending on direction)
  • elapsedMs is the time delta between pointerdown and pointerup events
  • Math.abs() ensures velocity is always a positive scalar value regardless of swipe direction

The repository specifies 0.11 px/ms as the dismissal threshold. When velocity exceeds this value, the gesture carries enough momentum to warrant dismissal, even if the user hasn't dragged the element past a fixed distance threshold.

Implementation Steps

Follow this exact sequence used throughout the emilkowalski/skills standards:

  1. Record the drag start time when the user begins the gesture (store Date.now() or performance.now()).
  2. Track the drag distance continuously while the pointer moves, updating the delta from the original coordinates.
  3. Capture the end state on pointer release, computing elapsedMs as the current time minus the start time.
  4. Compute velocity using the formula above.
  5. Evaluate against thresholds—if velocity > 0.11 or Math.abs(dragDistance) >= SWIPE_THRESHOLD, execute the dismiss animation.

Code Implementation Examples

Vanilla React Hook

This minimal implementation follows the pattern described in the source files, using React refs to avoid re-renders during the gesture:

import { useRef } from "react";

function SwipeDismiss({ onDismiss, children }) {
  const startX = useRef(0);
  const startTime = useRef(0);
  const SWIPE_THRESHOLD = 100; // px, safety fallback
  const VELOCITY_THRESHOLD = 0.11; // px/ms from skills/emil-design-eng

  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 (velocity > VELOCITY_THRESHOLD || Math.abs(distance) >= SWIPE_THRESHOLD) {
      onDismiss(); // Trigger spring-based exit animation
    }
  };

  return (
    <div
      onPointerDown={handlePointerDown}
      onPointerUp={handlePointerUp}
      style={{ touchAction: "none", userSelect: "none" }}
    >
      {children}
    </div>
  );
}

Framer Motion with Spring Physics

As documented in skills/review-animations/STANDARDS.md (lines 61-73), springs preserve velocity when interrupted, making them ideal for momentum gestures. This example combines the velocity calculation with Framer Motion's declarative API:

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

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

  const spring = useSpring(0, {
    stiffness: 300,
    damping: 30
  });

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

  const handleDragEnd = (event, 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 that respects current momentum
      spring.set(0);
    }
  };

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

Source Files and Standards

The velocity calculation and dismissal logic are defined across these specific files:

  • skills/emil-design-eng/SKILL.md (lines 441-452): Contains the exact velocity formula (Math.abs(dragDistance) / timeTaken) and the 0.11 px/ms threshold specification for momentum-based dismissal.
  • skills/review-animations/STANDARDS.md (lines 61-73): Documents why springs are the preferred animation primitive for preserving velocity during interruptible gestures.
  • skills/review-animations/STANDARDS.md (lines 37-44): Lists the velocity-based dismissal rule alongside damping and bounce recommendations for drag interactions.

Summary

  • Velocity equals absolute distance divided by elapsed milliseconds, producing a value in px/ms.
  • The dismissal threshold is 0.11 px/ms according to the emilkowalski/skills standards—any swipe faster than this should trigger dismissal.
  • Combine velocity with a distance fallback (e.g., 100px) to handle slow, deliberate drags that still cross the element's midpoint.
  • Use spring animations to preserve momentum when the gesture ends, allowing the UI to continue moving naturally before settling or dismissing.

Frequently Asked Questions

What units should I use for the velocity calculation?

The emilkowalski/skills repository specifies pixels per millisecond (px/ms). This produces small decimal values (like 0.11) that are easy to reason about and compare against thresholds. Always measure time in milliseconds, not seconds, to avoid floating-point confusion when dividing small distances by large time values.

Why use 0.11 px/ms as the threshold instead of a fixed distance?

A fixed distance threshold forces users to drag an element a specific number of pixels regardless of their intent. The 0.11 px/ms threshold (as defined in skills/emil-design-eng/SKILL.md) recognizes that a quick flick carries momentum and should dismiss the element even on small screens, while a slow drag that travels far might indicate hesitation rather than intent to dismiss.

How do I handle interruptions during the swipe?

According to skills/review-animations/STANDARDS.md (lines 61-73), you should use spring-based animations rather than time-based tweens. Springs naturally preserve velocity when interrupted, meaning if the user grabs the element mid-flight, the animation continues from the current velocity rather than resetting to zero.

Should I check velocity or distance first?

Check both simultaneously using an OR condition. The repository recommends using velocity as the primary signal but maintaining an optional distance fallback (Math.abs(swipeAmount) >= SWIPE_THRESHOLD) for robustness. This handles edge cases where very slow drags eventually cross a significant portion of the screen, indicating clear user intent despite low velocity.

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 →