React Swipe Detection How-To
Swipe gestures are crucial for mobile-first React applications. A swipe is a quick, directional finger movement—left, right, up, or down—that triggers navigation, dismissal, or state changes. Unlike dragging, which is continuous, a swipe is a discrete gesture with clear start and end. Detecting swipes accurately means measuring direction, velocity, and distance, then distinguishing them from accidental drags or scrolls.
Understanding Swipe Mechanics
A swipe consists of a pointer down, movement, and pointer up. The key variables are the initial position, final position, and elapsed time. Calculate the horizontal and vertical distance, then determine the dominant direction and whether it's fast enough to count as a swipe. Here's a practical swipe detector:
import { useState, useRef } from 'react';
export function useSwipeGesture(onSwipe) {
const startRef = useRef(null);
const [isMoving, setIsMoving] = useState(false);
const handleTouchStart = (e) => {
const touch = e.touches[0];
startRef.current = {
x: touch.clientX,
y: touch.clientY,
time: Date.now(),
};
setIsMoving(true);
};
const handleTouchEnd = (e) => {
if (!startRef.current) return;
const touch = e.changedTouches[0];
const endX = touch.clientX;
const endY = touch.clientY;
const endTime = Date.now();
const deltaX = endX - startRef.current.x;
const deltaY = endY - startRef.current.y;
const duration = endTime - startRef.current.time;
const distance = Math.hypot(deltaX, deltaY);
const velocity = distance / duration;
// Swipe threshold: > 50px, > 300ms, < 1s, > 0.5 px/ms
if (distance > 50 && duration > 300 && duration < 1000 && velocity > 0.5) {
const isHorizontal = Math.abs(deltaX) > Math.abs(deltaY);
const direction = isHorizontal
? deltaX > 0 ? 'right' : 'left'
: deltaY > 0 ? 'down' : 'up';
onSwipe({ direction, distance, velocity, deltaX, deltaY });
}
setIsMoving(false);
startRef.current = null;
};
return {
onTouchStart: handleTouchStart,
onTouchEnd: handleTouchEnd,
isMoving,
};
}
The thresholds ensure that tiny accidental movements and scrolls don't trigger swipe handlers. In 2026, swipe detection is essential for carousels, modals, and bottom sheets on mobile devices.
Building a Swipeable Carousel
A common use case is a carousel that advances on swipe. Combine swipe detection with state to track the current slide:
import React, { useState } from 'react';
import { useSwipeGesture } from './useSwipeGesture';
export default function SwipeableCarousel() {
const [slideIndex, setSlideIndex] = useState(0);
const slides = [
{ id: 1, color: '#ef4444', label: 'Slide 1' },
{ id: 2, color: '#3b82f6', label: 'Slide 2' },
{ id: 3, color: '#10b981', label: 'Slide 3' },
];
const handleSwipe = ({ direction }) => {
if (direction === 'left') {
setSlideIndex((prev) => (prev + 1) % slides.length);
} else if (direction === 'right') {
setSlideIndex((prev) => (prev - 1 + slides.length) % slides.length);
}
};
const swipeHandlers = useSwipeGesture(handleSwipe);
return (
<div
{...swipeHandlers}
style={{
width: '100%',
height: '300px',
overflow: 'hidden',
borderRadius: '8px',
backgroundColor: '#f9f9f9',
position: 'relative',
}}
>
<div
style={{
width: '100%',
height: '100%',
backgroundColor: slides[slideIndex].color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
fontWeight: 'bold',
color: '#fff',
transition: swipeHandlers.isMoving ? 'none' : 'background-color 0.3s',
}}
>
{slides[slideIndex].label}
</div>
<div
style={{
position: 'absolute',
bottom: '12px',
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
gap: '6px',
}}
>
{slides.map((_, idx) => (
<div
key={idx}
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: idx === slideIndex ? '#fff' : 'rgba(255,255,255,0.5)',
transition: 'background-color 0.3s',
}}
/>
))}
</div>
</div>
);
}
As the user swipes left, the carousel advances to the next slide. Swipe right goes backward. The dots at the bottom show the current position.
Detecting Multi-Directional Swipes
For applications that respond to swipes in all four directions (up, down, left, right), categorize the gesture based on the dominant axis:
export function useMultiSwipe(onSwipe) {
const startRef = useRef(null);
const handleTouchStart = (e) => {
const touch = e.touches[0];
startRef.current = {
x: touch.clientX,
y: touch.clientY,
time: Date.now(),
};
};
const handleTouchEnd = (e) => {
if (!startRef.current) return;
const touch = e.changedTouches[0];
const deltaX = touch.clientX - startRef.current.x;
const deltaY = touch.clientY - startRef.current.y;
const duration = Date.now() - startRef.current.time;
const distance = Math.hypot(deltaX, deltaY);
if (distance > 50 && duration < 300) {
const angle = Math.atan2(deltaY, deltaX);
const degrees = (angle * 180) / Math.PI;
// Determine direction based on angle
let direction;
if (degrees > -45 && degrees < 45) direction = 'right';
else if (degrees >= 45 && degrees < 135) direction = 'down';
else if (degrees >= 135 || degrees < -135) direction = 'left';
else direction = 'up';
onSwipe({ direction, distance, deltaX, deltaY });
}
startRef.current = null;
};
return { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd };
}
By calculating the angle using Math.atan2, you can precisely determine the swipe direction. This is useful for games, navigation menus, or touch-driven interfaces that respond to all four directions.
Key Takeaways
- A swipe is a fast, directional touch movement; set thresholds for distance (50+ px), duration (300–1000ms), and velocity (0.5+ px/ms) to avoid false positives.
- Use
e.changedTouches[0]to get the final touch position ontouchend, since touch points are cleared after the event. - Calculate the angle using
Math.atan2to determine the dominant swipe direction (left, right, up, down) with precision. - Combine swipe detection with carousel or menu state to create responsive mobile experiences.
Frequently Asked Questions
How do I prevent swipe handlers from conflicting with scroll?
Check the distance and velocity early. If the user moves slowly or barely moves, don't treat it as a swipe—let the browser handle scrolling. Additionally, you can use e.preventDefault() only after confirming the gesture is a swipe, not during the touch movement (which blocks scroll).
Can I use swipe detection with mouse events?
Yes, swipe detection works with pointer or mouse events using the same distance/time logic. However, the typical use case is touch on mobile. If you need both, use the pointer events API and apply the same thresholds.
What if my app is inside an iframe or has nested scrollable containers?
Event propagation can be tricky. Use e.stopPropagation() on the swipe handler to prevent bubbling up to parent scrollers. Test on both mobile and desktop, and ensure touch-action CSS is appropriate (e.g., touch-action: none on your swipe container).
How do I add a velocity-based "fling" animation?
After detecting the swipe, calculate velocity = distance / duration. Use that velocity to drive an animation library like Framer Motion or Reanimated. The higher the velocity, the longer the fling animation duration, creating a natural flick effect.
Should I use a library like React Swipeable instead?
Libraries handle edge cases and device variations. For production apps with many swipe interactions, a library is worth the dependency. For a single carousel or modal, the custom hook approach is lightweight and sufficient.