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
- Velocity calculation: Divide absolute drag distance by elapsed milliseconds to get px/ms speed
- Critical threshold: Trigger dismissal when velocity exceeds 0.11 px/ms
- Safety fallback: Combine with a distance threshold (e.g., 100px) for deliberate slow swipes
- Animation type: Use spring physics from
skills/review-animations/STANDARDS.mdto maintain momentum during interrupts - Source files: Reference
skills/emil-design-eng/SKILL.mdfor the velocity formula andskills/review-animations/STANDARDS.mdfor animation standards
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →