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:
dragDistanceis the total pixels moved from pointer start to end (positive or negative depending on direction)elapsedMsis the time delta betweenpointerdownandpointerupeventsMath.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:
- Record the drag start time when the user begins the gesture (store
Date.now()orperformance.now()). - Track the drag distance continuously while the pointer moves, updating the delta from the original coordinates.
- Capture the end state on pointer release, computing
elapsedMsas the current time minus the start time. - Compute velocity using the formula above.
- Evaluate against thresholds—if
velocity > 0.11orMath.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/skillsstandards—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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →