How can you detect that a user swiped his finger in some direction over a web page with JavaScript?
I was wondering if there was one solution that would work for websites on both the iPhone and an Android phone.
How can you detect that a user swiped his finger in some direction over a web page with JavaScript?
I was wondering if there was one solution that would work for websites on both the iPhone and an Android phone.
Simple vanilla JS code sample:
document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
var xDown = null;
var yDown = null;
function getTouches(evt) {
return evt.touches || // browser API
evt.originalEvent.touches; // jQuery
}
function handleTouchStart(evt) {
const firstTouch = getTouches(evt)[0];
xDown = firstTouch.clientX;
yDown = firstTouch.clientY;
};
function handleTouchMove(evt) {
if ( ! xDown || ! yDown ) {
return;
}
var xUp = evt.touches[0].clientX;
var yUp = evt.touches[0].clientY;
var xDiff = xDown - xUp;
var yDiff = yDown - yUp;
if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
if ( xDiff > 0 ) {
/* right swipe */
} else {
/* left swipe */
}
} else {
if ( yDiff > 0 ) {
/* down swipe */
} else {
/* up swipe */
}
}
/* reset values */
xDown = null;
yDown = null;
};
Tested in Android.
touchstart
, touchmove
?
Simple vanilla JS example for horizontal swipe:
let touchstartX = 0
let touchendX = 0
function checkDirection() {
if (touchendX < touchstartX) alert('swiped left!')
if (touchendX > touchstartX) alert('swiped right!')
}
document.addEventListener('touchstart', e => {
touchstartX = e.changedTouches[0].screenX
})
document.addEventListener('touchend', e => {
touchendX = e.changedTouches[0].screenX
checkDirection()
})
You can use pretty same logic for vertical swipe.
I merged a few of the answers here into a script that uses CustomEvent to fire swiped events in the DOM. Add the 0.7k swiped-events.min.js script to your page and listen for swiped events:
document.addEventListener('swiped', function(e) {
console.log(e.target); // the element that was swiped
console.log(e.detail.dir); // swiped direction
});
document.addEventListener('swiped-left', function(e) {
console.log(e.target); // the element that was swiped
});
document.addEventListener('swiped-right', function(e) {
console.log(e.target); // the element that was swiped
});
document.addEventListener('swiped-up', function(e) {
console.log(e.target); // the element that was swiped
});
document.addEventListener('swiped-down', function(e) {
console.log(e.target); // the element that was swiped
});
You can also attach directly to an element:
document.getElementById('myBox').addEventListener('swiped-down', function(e) {
console.log(e.target); // the element that was swiped
});
You can specify the following attributes to tweak how swipe interaction functions in your page (these are optional).
<div data-swipe-threshold="10"
data-swipe-timeout="1000"
data-swipe-ignore="false">
Swiper, get swiping!
</div>
To set defaults application wide, set config attributes on topmost element:
<body data-swipe-threshold="100" data-swipe-timeout="250">
<div>Swipe me</div>
<div>or me</div>
</body>
Source code is available on Github
Based on @givanse's answer, this is how you could do it with classes
:
class Swipe {
constructor(element) {
this.xDown = null;
this.yDown = null;
this.element = typeof(element) === 'string' ? document.querySelector(element) : element;
this.element.addEventListener('touchstart', function(evt) {
this.xDown = evt.touches[0].clientX;
this.yDown = evt.touches[0].clientY;
}.bind(this), false);
}
onLeft(callback) {
this.onLeft = callback;
return this;
}
onRight(callback) {
this.onRight = callback;
return this;
}
onUp(callback) {
this.onUp = callback;
return this;
}
onDown(callback) {
this.onDown = callback;
return this;
}
handleTouchMove(evt) {
if ( ! this.xDown || ! this.yDown ) {
return;
}
var xUp = evt.touches[0].clientX;
var yUp = evt.touches[0].clientY;
this.xDiff = this.xDown - xUp;
this.yDiff = this.yDown - yUp;
if ( Math.abs( this.xDiff ) > Math.abs( this.yDiff ) ) { // Most significant.
if ( this.xDiff > 0 ) {
this.onLeft();
} else {
this.onRight();
}
} else {
if ( this.yDiff > 0 ) {
this.onUp();
} else {
this.onDown();
}
}
// Reset values.
this.xDown = null;
this.yDown = null;
}
run() {
this.element.addEventListener('touchmove', function(evt) {
this.handleTouchMove(evt).bind(this);
}.bind(this), false);
}
}
You can than use it like this:
// Use class to get element by string.
var swiper = new Swipe('#my-element');
swiper.onLeft(function() { alert('You swiped left.') });
swiper.run();
// Get the element yourself.
var swiper = new Swipe(document.getElementById('#my-element'));
swiper.onLeft(function() { alert('You swiped left.') });
swiper.run();
// One-liner.
(new Swipe('#my-element')).onLeft(function() { alert('You swiped left.') }).run();
.bind
of undefined because your handleTouchMove
actually didn't return anything. also it's useless to call bind when calling function with this.
because it's already bound to current context
Oct 14, 2016 at 12:02
.bind(this);
and it worked gracefully. thank you @nicholas_r
Jul 29, 2017 at 14:58
touches[0]
to changedTouches[0]
and the event handler type handleTouchMove
to handleTouchEnd
I have found @givanse brilliant answer to be the most reliable and compatible across multiple mobile browsers for registering swipe actions.
However, there's a change in his code required to make it work in modern day mobile browsers that are using jQuery
.
event.touches
won't exist if jQuery
is used and results in undefined
and should be replaced by event.originalEvent.touches
. Without jQuery
, event.touches
should work fine.
So the solution becomes,
document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
var xDown = null;
var yDown = null;
function handleTouchStart(evt) {
xDown = evt.originalEvent.touches[0].clientX;
yDown = evt.originalEvent.touches[0].clientY;
};
function handleTouchMove(evt) {
if ( ! xDown || ! yDown ) {
return;
}
var xUp = evt.originalEvent.touches[0].clientX;
var yUp = evt.originalEvent.touches[0].clientY;
var xDiff = xDown - xUp;
var yDiff = yDown - yUp;
if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
if ( xDiff > 0 ) {
/* left swipe */
} else {
/* right swipe */
}
} else {
if ( yDiff > 0 ) {
/* up swipe */
} else {
/* down swipe */
}
}
/* reset values */
xDown = null;
yDown = null;
};
Tested on:
event.originalEvent
. The thing is event.touches
has ceased to exist now and results in undefined
.
May 21, 2017 at 20:35
event.originalEvent
. I will update my answer. Thanks! :)
May 22, 2017 at 11:06
what i've used before is you have to detect the mousedown event, record its x,y location (whichever is relevant) then detect the mouseup event, and subtract the two values.
Some mod of uppest answer(can't comment...) to deal with to short swipes
document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
var xDown = null;
var yDown = null;
function handleTouchStart(evt) {
xDown = evt.touches[0].clientX;
yDown = evt.touches[0].clientY;
};
function handleTouchMove(evt) {
if ( ! xDown || ! yDown ) {
return;
}
var xUp = evt.touches[0].clientX;
var yUp = evt.touches[0].clientY;
var xDiff = xDown - xUp;
var yDiff = yDown - yUp;
if(Math.abs( xDiff )+Math.abs( yDiff )>150){ //to deal with to short swipes
if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
if ( xDiff > 0 ) {/* left swipe */
alert('left!');
} else {/* right swipe */
alert('right!');
}
} else {
if ( yDiff > 0 ) {/* up swipe */
alert('Up!');
} else { /* down swipe */
alert('Down!');
}
}
/* reset values */
xDown = null;
yDown = null;
}
};
if ( ! xDown || ! yDown || e.touches.length === 2 ) {
for horizontal pinch zoom
jQuery Mobile also includes swipe support: http://api.jquerymobile.com/swipe/
Example
$("#divId").on("swipe", function(event) {
alert("It's a swipe!");
});
I wanted to detect left and right swipe only, but trigger the action only when the touch event ends, so I slightly modified the @givanse's great answer to do that.
Why to do that? If for example, while swiping, the user notices he finally doesn't want to swipe, he can move his finger at the original position (a very popular "dating" phone application does this ;)), and then the "swipe right" event is cancelled.
So in order to avoid a "swipe right" event just because there is a 3px difference horizontally, I added a threshold under which an event is discarded: in order to have a "swipe right" event, the user has to swipe of at least 1/3 of the browser width (of course you can modify this).
All these small details enhance the user experience.
Note that currently, a "touch pinch zoom" might be detected as a swipe if one of the two fingers does a big horizontal move during the pinch zoom.
Here is the (Vanilla JS) code:
var xDown = null, yDown = null, xUp = null, yUp = null;
document.addEventListener('touchstart', touchstart, false);
document.addEventListener('touchmove', touchmove, false);
document.addEventListener('touchend', touchend, false);
function touchstart(evt) { const firstTouch = (evt.touches || evt.originalEvent.touches)[0]; xDown = firstTouch.clientX; yDown = firstTouch.clientY; }
function touchmove(evt) { if (!xDown || !yDown ) return; xUp = evt.touches[0].clientX; yUp = evt.touches[0].clientY; }
function touchend(evt) {
var xDiff = xUp - xDown, yDiff = yUp - yDown;
if ((Math.abs(xDiff) > Math.abs(yDiff)) && (Math.abs(xDiff) > 0.33 * document.body.clientWidth)) {
if (xDiff < 0)
document.getElementById('leftnav').click();
else
document.getElementById('rightnav').click();
}
xDown = null, yDown = null;
}
threshold, timeout swipe, swipeBlockElems add.
document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
document.addEventListener('touchend', handleTouchEnd, false);
const SWIPE_BLOCK_ELEMS = [
'swipBlock',
'handle',
'drag-ruble'
]
let xDown = null;
let yDown = null;
let xDiff = null;
let yDiff = null;
let timeDown = null;
const TIME_THRESHOLD = 200;
const DIFF_THRESHOLD = 130;
function handleTouchEnd() {
let timeDiff = Date.now() - timeDown;
if (Math.abs(xDiff) > Math.abs(yDiff)) { /*most significant*/
if (Math.abs(xDiff) > DIFF_THRESHOLD && timeDiff < TIME_THRESHOLD) {
if (xDiff > 0) {
// console.log(xDiff, TIME_THRESHOLD, DIFF_THRESHOLD)
SWIPE_LEFT(LEFT) /* left swipe */
} else {
// console.log(xDiff)
SWIPE_RIGHT(RIGHT) /* right swipe */
}
} else {
console.log('swipeX trashhold')
}
} else {
if (Math.abs(yDiff) > DIFF_THRESHOLD && timeDiff < TIME_THRESHOLD) {
if (yDiff > 0) {
/* up swipe */
} else {
/* down swipe */
}
} else {
console.log('swipeY trashhold')
}
}
/* reset values */
xDown = null;
yDown = null;
timeDown = null;
}
function containsClassName (evntarget , classArr) {
for (var i = classArr.length - 1; i >= 0; i--) {
if( evntarget.classList.contains(classArr[i]) ) {
return true;
}
}
}
function handleTouchStart(evt) {
let touchStartTarget = evt.target;
if( containsClassName(touchStartTarget, SWIPE_BLOCK_ELEMS) ) {
return;
}
timeDown = Date.now()
xDown = evt.touches[0].clientX;
yDown = evt.touches[0].clientY;
xDiff = 0;
yDiff = 0;
}
function handleTouchMove(evt) {
if (!xDown || !yDown) {
return;
}
var xUp = evt.touches[0].clientX;
var yUp = evt.touches[0].clientY;
xDiff = xDown - xUp;
yDiff = yDown - yUp;
}
Adding to this answer here. This one adds support for mouse events for testing on desktop:
<!--scripts-->
class SwipeEventDispatcher {
constructor(element, options = {}) {
this.evtMap = {
SWIPE_LEFT: [],
SWIPE_UP: [],
SWIPE_DOWN: [],
SWIPE_RIGHT: []
};
this.xDown = null;
this.yDown = null;
this.element = element;
this.isMouseDown = false;
this.listenForMouseEvents = true;
this.options = Object.assign({ triggerPercent: 0.3 }, options);
element.addEventListener('touchstart', evt => this.handleTouchStart(evt), false);
element.addEventListener('touchend', evt => this.handleTouchEnd(evt), false);
element.addEventListener('mousedown', evt => this.handleMouseDown(evt), false);
element.addEventListener('mouseup', evt => this.handleMouseUp(evt), false);
}
on(evt, cb) {
this.evtMap[evt].push(cb);
}
off(evt, lcb) {
this.evtMap[evt] = this.evtMap[evt].filter(cb => cb !== lcb);
}
trigger(evt, data) {
this.evtMap[evt].map(handler => handler(data));
}
handleTouchStart(evt) {
this.xDown = evt.touches[0].clientX;
this.yDown = evt.touches[0].clientY;
}
handleMouseDown(evt) {
if (this.listenForMouseEvents==false) return;
this.xDown = evt.clientX;
this.yDown = evt.clientY;
this.isMouseDown = true;
}
handleMouseUp(evt) {
if (this.isMouseDown == false) return;
const deltaX = evt.clientX - this.xDown;
const deltaY = evt.clientY - this.yDown;
const distMoved = Math.abs(Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY);
const activePct = distMoved / this.element.offsetWidth;
if (activePct > this.options.triggerPercent) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
deltaX < 0 ? this.trigger('SWIPE_LEFT') : this.trigger('SWIPE_RIGHT');
} else {
deltaY > 0 ? this.trigger('SWIPE_UP') : this.trigger('SWIPE_DOWN');
}
}
}
handleTouchEnd(evt) {
const deltaX = evt.changedTouches[0].clientX - this.xDown;
const deltaY = evt.changedTouches[0].clientY - this.yDown;
const distMoved = Math.abs(Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY);
const activePct = distMoved / this.element.offsetWidth;
if (activePct > this.options.triggerPercent) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
deltaX < 0 ? this.trigger('SWIPE_LEFT') : this.trigger('SWIPE_RIGHT');
} else {
deltaY > 0 ? this.trigger('SWIPE_UP') : this.trigger('SWIPE_DOWN');
}
}
}
}
// add a listener on load
window.addEventListener("load", function(event) {
const dispatcher = new SwipeEventDispatcher(document.body);
dispatcher.on('SWIPE_RIGHT', () => { console.log('I swiped right!') })
dispatcher.on('SWIPE_LEFT', () => { console.log('I swiped left!') })
});
If anyone is trying to use jQuery Mobile on Android and has problems with JQM swipe detection
(I had some on Xperia Z1, Galaxy S3, Nexus 4 and some Wiko phones too) this can be useful :
//Fix swipe gesture on android
if(android){ //Your own device detection here
$.event.special.swipe.verticalDistanceThreshold = 500
$.event.special.swipe.horizontalDistanceThreshold = 10
}
Swipe on android wasn't detected unless it was a very long, precise and fast swipe.
With these two lines it works correctly
$.event.special.swipe.scrollSupressionThreshold = 8;
but you put me in the right direction! Thank you!
I had trouble with touchend handler firing continuously while the user was dragging a finger around. I don't know if that's due to something I'm doing wrong or not but I rewired this to accumulate moves with touchmove and touchend actually fires the callback.
I also needed to have a large number of these instances and so I added enable/disable methods.
And a threshold where a short swipe doesn't fire. Touchstart zero's the counters each time.
You can change the target_node on the fly. Enable on creation is optional.
/** Usage: */
touchevent = new Modules.TouchEventClass(callback, target_node);
touchevent.enable();
touchevent.disable();
/**
*
* Touch event module
*
* @param method set_target_mode
* @param method __touchstart
* @param method __touchmove
* @param method __touchend
* @param method enable
* @param method disable
* @param function callback
* @param node target_node
*/
Modules.TouchEventClass = class {
constructor(callback, target_node, enable=false) {
/** callback function */
this.callback = callback;
this.xdown = null;
this.ydown = null;
this.enabled = false;
this.target_node = null;
/** move point counts [left, right, up, down] */
this.counts = [];
this.set_target_node(target_node);
/** Enable on creation */
if (enable === true) {
this.enable();
}
}
/**
* Set or reset target node
*
* @param string/node target_node
* @param string enable (optional)
*/
set_target_node(target_node, enable=false) {
/** check if we're resetting target_node */
if (this.target_node !== null) {
/** remove old listener */
this.disable();
}
/** Support string id of node */
if (target_node.nodeName === undefined) {
target_node = document.getElementById(target_node);
}
this.target_node = target_node;
if (enable === true) {
this.enable();
}
}
/** enable listener */
enable() {
this.enabled = true;
this.target_node.addEventListener("touchstart", this.__touchstart.bind(this));
this.target_node.addEventListener("touchmove", this.__touchmove.bind(this));
this.target_node.addEventListener("touchend", this.__touchend.bind(this));
}
/** disable listener */
disable() {
this.enabled = false;
this.target_node.removeEventListener("touchstart", this.__touchstart);
this.target_node.removeEventListener("touchmove", this.__touchmove);
this.target_node.removeEventListener("touchend", this.__touchend);
}
/** Touchstart */
__touchstart(event) {
event.stopPropagation();
this.xdown = event.touches[0].clientX;
this.ydown = event.touches[0].clientY;
/** reset count of moves in each direction, [left, right, up, down] */
this.counts = [0, 0, 0, 0];
}
/** Touchend */
__touchend(event) {
let max_moves = Math.max(...this.counts);
if (max_moves > 500) { // set this threshold appropriately
/** swipe happened */
let index = this.counts.indexOf(max_moves);
if (index == 0) {
this.callback("left");
} else if (index == 1) {
this.callback("right");
} else if (index == 2) {
this.callback("up");
} else {
this.callback("down");
}
}
}
/** Touchmove */
__touchmove(event) {
event.stopPropagation();
if (! this.xdown || ! this.ydown) {
return;
}
let xup = event.touches[0].clientX;
let yup = event.touches[0].clientY;
let xdiff = this.xdown - xup;
let ydiff = this.ydown - yup;
/** Check x or y has greater distance */
if (Math.abs(xdiff) > Math.abs(ydiff)) {
if (xdiff > 0) {
this.counts[0] += Math.abs(xdiff);
} else {
this.counts[1] += Math.abs(xdiff);
}
} else {
if (ydiff > 0) {
this.counts[2] += Math.abs(ydiff);
} else {
this.counts[3] += Math.abs(ydiff);
}
}
}
}
If you just need swipe, you are better off size wise just using only the part you need. This should work on any touch device.
This is ~450 bytes' after gzip compression, minification, babel etc.
I wrote the below class based on the other answers, it uses percentage moved instead of pixels, and a event dispatcher pattern to hook/unhook things.
Use it like so:
const dispatcher = new SwipeEventDispatcher(myElement);
dispatcher.on('SWIPE_RIGHT', () => { console.log('I swiped right!') })
export class SwipeEventDispatcher {
constructor(element, options = {}) {
this.evtMap = {
SWIPE_LEFT: [],
SWIPE_UP: [],
SWIPE_DOWN: [],
SWIPE_RIGHT: []
};
this.xDown = null;
this.yDown = null;
this.element = element;
this.options = Object.assign({ triggerPercent: 0.3 }, options);
element.addEventListener('touchstart', evt => this.handleTouchStart(evt), false);
element.addEventListener('touchend', evt => this.handleTouchEnd(evt), false);
}
on(evt, cb) {
this.evtMap[evt].push(cb);
}
off(evt, lcb) {
this.evtMap[evt] = this.evtMap[evt].filter(cb => cb !== lcb);
}
trigger(evt, data) {
this.evtMap[evt].map(handler => handler(data));
}
handleTouchStart(evt) {
this.xDown = evt.touches[0].clientX;
this.yDown = evt.touches[0].clientY;
}
handleTouchEnd(evt) {
const deltaX = evt.changedTouches[0].clientX - this.xDown;
const deltaY = evt.changedTouches[0].clientY - this.yDown;
const distMoved = Math.abs(Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY);
const activePct = distMoved / this.element.offsetWidth;
if (activePct > this.options.triggerPercent) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
deltaX < 0 ? this.trigger('SWIPE_LEFT') : this.trigger('SWIPE_RIGHT');
} else {
deltaY > 0 ? this.trigger('SWIPE_UP') : this.trigger('SWIPE_DOWN');
}
}
}
}
export default SwipeEventDispatcher;
I have merged a few of the answers too, mostly the first one and the second with classes, and here is my version:
export default class Swipe {
constructor(options) {
this.xDown = null;
this.yDown = null;
this.options = options;
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
document.addEventListener('touchstart', this.handleTouchStart, false);
document.addEventListener('touchmove', this.handleTouchMove, false);
}
onLeft() {
this.options.onLeft();
}
onRight() {
this.options.onRight();
}
onUp() {
this.options.onUp();
}
onDown() {
this.options.onDown();
}
static getTouches(evt) {
return evt.touches // browser API
}
handleTouchStart(evt) {
const firstTouch = Swipe.getTouches(evt)[0];
this.xDown = firstTouch.clientX;
this.yDown = firstTouch.clientY;
}
handleTouchMove(evt) {
if ( ! this.xDown || ! this.yDown ) {
return;
}
let xUp = evt.touches[0].clientX;
let yUp = evt.touches[0].clientY;
let xDiff = this.xDown - xUp;
let yDiff = this.yDown - yUp;
if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
if ( xDiff > 0 && this.options.onLeft) {
/* left swipe */
this.onLeft();
} else if (this.options.onRight) {
/* right swipe */
this.onRight();
}
} else {
if ( yDiff > 0 && this.options.onUp) {
/* up swipe */
this.onUp();
} else if (this.options.onDown){
/* down swipe */
this.onDown();
}
}
/* reset values */
this.xDown = null;
this.yDown = null;
}
}
Afterward can use it as the following:
let swiper = new Swipe({
onLeft() {
console.log('You swiped left.');
}
});
It helps to avoid console errors when you want to call only let's say "onLeft" method.
Used two:
jQuery mobile: work in most of cases and specially when you are developing applicaion which uses other jQuery plugin then better to use jQuery mobile controls for this. Visit it here: https://www.w3schools.com/jquerymobile/jquerymobile_events_touch.asp
Hammer Time ! one of the best,lightweight and fast javascript based library. Visit it here: https://hammerjs.github.io/
I reworked @givanse's solution to function as a React hook. Input is some optional event listeners, output is a functional ref (needs to be functional so the hook can re-run when/if the ref changes).
Also added in vertical/horizontal swipe threshold param, so that small motions don't accidentally trigger the event listeners, but these can be set to 0 to mimic original answer more closely.
Tip: for best performance, the event listener input functions should be memoized.
function useSwipeDetector({
// Event listeners.
onLeftSwipe,
onRightSwipe,
onUpSwipe,
onDownSwipe,
// Threshold to detect swipe.
verticalSwipeThreshold = 50,
horizontalSwipeThreshold = 30,
}) {
const [domRef, setDomRef] = useState(null);
const xDown = useRef(null);
const yDown = useRef(null);
useEffect(() => {
if (!domRef) {
return;
}
function handleTouchStart(evt) {
const [firstTouch] = evt.touches;
xDown.current = firstTouch.clientX;
yDown.current = firstTouch.clientY;
};
function handleTouchMove(evt) {
if (!xDown.current || !yDown.current) {
return;
}
const [firstTouch] = evt.touches;
const xUp = firstTouch.clientX;
const yUp = firstTouch.clientY;
const xDiff = xDown.current - xUp;
const yDiff = yDown.current - yUp;
if (Math.abs(xDiff) > Math.abs(yDiff)) {/*most significant*/
if (xDiff > horizontalSwipeThreshold) {
if (onRightSwipe) onRightSwipe();
} else if (xDiff < -horizontalSwipeThreshold) {
if (onLeftSwipe) onLeftSwipe();
}
} else {
if (yDiff > verticalSwipeThreshold) {
if (onUpSwipe) onUpSwipe();
} else if (yDiff < -verticalSwipeThreshold) {
if (onDownSwipe) onDownSwipe();
}
}
};
function handleTouchEnd() {
xDown.current = null;
yDown.current = null;
}
domRef.addEventListener("touchstart", handleTouchStart, false);
domRef.addEventListener("touchmove", handleTouchMove, false);
domRef.addEventListener("touchend", handleTouchEnd, false);
return () => {
domRef.removeEventListener("touchstart", handleTouchStart);
domRef.removeEventListener("touchmove", handleTouchMove);
domRef.removeEventListener("touchend", handleTouchEnd);
};
}, [domRef, onLeftSwipe, onRightSwipe, onUpSwipe, onDownSwipe, verticalSwipeThreshold, horizontalSwipeThreshold]);
return (ref) => setDomRef(ref);
};
On top of what was suggested here, I would keep track of finger numbers, because if you touch with two fingers at the same time, it will pick up the X position without the ~swipe~ motion which leads to a weird behavior, and also, you could want to set a "distance" minimum so the user won't trigger the swipe by mistake when touch through your website or application.
//Swipe
let touchstartX = 0
let touchendX = 0
let fingerCount = 0
const checkDirection = () => {
const distance = 50 //Minimum distance for the swipe to work
//left
if (touchendX < touchstartX && (touchstartX - touchendX) > distance ){
//Do something cool
}
//right
if (touchendX > touchstartX && (touchendX - touchstartX) > distance){
//Do something cooler
}
document.addEventListener('touchstart', e => {
fingerCount = e.touches.length
touchstartX = e.changedTouches[0].clientX
})
document.addEventListener('touchend', e => {
touchendX = e.changedTouches[0].clientX
if(fingerCount === 1){
checkDirection()
}
})
You might have an easier time first implementing it with mouse events to prototype.
There are many answers here, including the top, should be used with caution as they do not consider edge cases especially around bounding boxes.
See:
You will need to experiment to catch edge cases and behaviours such as the pointer moving outside of the element before ending.
A swipe is a very basic gesture which is a higher level of interface pointer interaction processing roughly sitting between processing raw events and handwriting recognition.
There's no single exact method for detecting a swipe or a fling though virtually all generally follow a basic principle of detecting a motion across an element with a threshold of distance and speed or velocity. You might simply say that if there is a movement across 65% of the screen size in a given direction within a given time then it is a swipe. Exactly where you draw the line and how you calculate it is up to you.
Some might also look at it from the perspective of momentum in a direction and how far off the screen it has been pushed when the element is released. This is clearer with sticky swipes where the element can be dragged and then on release will either bounce back or fly off the screen as if the elastic broke.
It's probably ideal to try to find a gesture library that you can either port or reuse that's commonly used for consistency. Many of the examples here are excessively simplistic, registering a swipe as the slightest touch in any direction.
Android would be the obvious choice though has the opposite problem, it's overly complex.
Many people appear to have misinterpreted the question as any movement in a direction. A swipe is a broad and relatively brief motion overwhelmingly in a single direction (though may be arced and have certain acceleration properties). A fling is similar though intends to casually propel an item away a fair distance under its own momentum.
The two are sufficiently similar that some libraries might only provide fling or swipe, which can be used interchangeably. On a flat screen it's difficult to truly separate the two gestures and generally speaking people are doing both (swiping the physical screen but flinging the UI element displayed on the screen).
You best option is to not do it yourself. There are already a large number of JavaScript libraries for detecting simple gestures.
An example of how to use with offset.
// at least 100 px are a swipe
// you can use the value relative to screen size: window.innerWidth * .1
const offset = 100;
let xDown, yDown
window.addEventListener('touchstart', e => {
const firstTouch = getTouch(e);
xDown = firstTouch.clientX;
yDown = firstTouch.clientY;
});
window.addEventListener('touchend', e => {
if (!xDown || !yDown) {
return;
}
const {
clientX: xUp,
clientY: yUp
} = getTouch(e);
const xDiff = xDown - xUp;
const yDiff = yDown - yUp;
const xDiffAbs = Math.abs(xDown - xUp);
const yDiffAbs = Math.abs(yDown - yUp);
// at least <offset> are a swipe
if (Math.max(xDiffAbs, yDiffAbs) < offset ) {
return;
}
if (xDiffAbs > yDiffAbs) {
if ( xDiff > 0 ) {
console.log('left');
} else {
console.log('right');
}
} else {
if ( yDiff > 0 ) {
console.log('up');
} else {
console.log('down');
}
}
});
function getTouch (e) {
return e.changedTouches[0]
}
I had to write a simple script for a carousel to detect swipe left or right.
I utilised Pointer Events instead of Touch Events.
I hope this is useful to individuals and I welcome any insights to improve my code; I feel rather sheepish to join this thread with significantly superior JS developers.
function getSwipeX({elementId}) {
this.e = document.getElementsByClassName(elementId)[0];
this.initialPosition = 0;
this.lastPosition = 0;
this.threshold = 200;
this.diffInPosition = null;
this.diffVsThreshold = null;
this.gestureState = 0;
this.getTouchStart = (event) => {
event.preventDefault();
if (window.PointerEvent) {
this.e.setPointerCapture(event.pointerId);
}
return this.initalTouchPos = this.getGesturePoint(event);
}
this.getTouchMove = (event) => {
event.preventDefault();
return this.lastPosition = this.getGesturePoint(event);
}
this.getTouchEnd = (event) => {
event.preventDefault();
if (window.PointerEvent) {
this.e.releasePointerCapture(event.pointerId);
}
this.doSomething();
this.initialPosition = 0;
}
this.getGesturePoint = (event) => {
this.point = event.pageX
return this.point;
}
this.whatGestureDirection = (event) => {
this.diffInPosition = this.initalTouchPos - this.lastPosition;
this.diffVsThreshold = Math.abs(this.diffInPosition) > this.threshold;
(Math.sign(this.diffInPosition) > 0) ? this.gestureState = 'L' : (Math.sign(this.diffInPosition) < 0) ? this.gestureState = 'R' : this.gestureState = 'N';
return [this.diffInPosition, this.diffVsThreshold, this.gestureState];
}
this.doSomething = (event) => {
let [gestureDelta,gestureThreshold,gestureDirection] = this.whatGestureDirection();
// USE THIS TO DEBUG
console.log(gestureDelta,gestureThreshold,gestureDirection);
if (gestureThreshold) {
(gestureDirection == 'L') ? // LEFT ACTION : // RIGHT ACTION
}
}
if (window.PointerEvent) {
this.e.addEventListener('pointerdown', this.getTouchStart, true);
this.e.addEventListener('pointermove', this.getTouchMove, true);
this.e.addEventListener('pointerup', this.getTouchEnd, true);
this.e.addEventListener('pointercancel', this.getTouchEnd, true);
}
}
You can call the function using new.
window.addEventListener('load', () => {
let test = new getSwipeX({
elementId: 'your_div_here'
});
})
I reworked @ruben-martinez answer for using the amazing solution from @givanse for handling swipe events using custom react hooks.
import React, { useEffect, useRef, useState } from "react";
export default function useSwiper() {
const [domRef, setDomRef] = useState<any>();
const xDown: React.MutableRefObject<number | null> = useRef(null);
const yDown: React.MutableRefObject<number | null> = useRef(null);
useEffect(() => {
if (!domRef) return;
function getTouches(event: React.TouchEvent<HTMLDivElement>) {
return event.touches;
}
function handleTouchStart(event: any) {
const firstTouch = getTouches(event)[0];
xDown.current = firstTouch.clientX;
yDown.current = firstTouch.clientY;
}
function handleTouchMove(event: React.TouchEvent<HTMLDivElement>) {
if (!xDown.current || !yDown.current) return;
const firstTouch = getTouches(event)[0];
const xUp = firstTouch.clientX;
const yUp = firstTouch.clientY;
const xDiff = xDown.current - xUp;
const yDiff = yDown.current - yUp;
if (Math.abs(xDiff) > Math.abs(yDiff)) {
// handle horizontal swipes
if (xDiff > 0) {
// we swiped right
console.log("right");
} else {
// we swiped left
console.log("left");
}
} else {
// handle vertical swipes
if (yDiff > 0) {
// we swiped down
console.log("down");
} else {
// we swiped up
console.log("up");
}
}
}
function handleTouchEnd(event: React.TouchEvent<HTMLDivElement>) {
xDown.current = null;
yDown.current = null;
}
domRef.addEventListener("touchstart", handleTouchStart, false);
domRef.addEventListener("touchmove", handleTouchMove, false);
domRef.addEventListener("touchend", handleTouchEnd, false);
return () => {
domRef.removeEventListener("touchstart", handleTouchStart, false);
domRef.removeEventListener("touchmove", handleTouchMove, false);
domRef.removeEventListener("touchend", handleTouchEnd, false);
};
}, [domRef]);
return (ref: any) => setDomRef(ref);
}
My major challenge with implementing his answer was not knowing how to bind the swipe element's ref to the ref from the custom hook.
Basically, what is happening is that we return a function from the custom hook. This function would allow us pass in a ref from the element we want to listen to swipe actions on. The custom hook on receipt of the ref then updates the hook state with the element's ref which triggers a re render so we have the actual element!
This functional ref style also allows us to use the hook for multiple elements. As shown below, I wanted to use it for a list of items to enable swipe to delete :)
import useSwiper from "./hooks/useSwipe";
const EntryCard = ({ entry, godMode, reload }: EntryProps) => {
const swiperRef = useSwiper();
const handleEntryClick =
(entry: Entry) => async (event: React.MouseEvent<HTMLDivElement>) => {
if (!godMode) return;
try {
reload((state) => !state);
} catch (err) {
console.log("Error deleting entry: ", err);
}
};
return (
<div className="item" onClick={handleEntryClick(entry)} ref={swiperRef}>
<div className="username">{entry.userName}</div>
<div className="score">{entry.weekScore}</div>
</div>
);
};
PS: You can pass in functions to your hook to receive the swipe values. Thank YOU :) Vote if you like :)
handle by touchStart and touchEnd :
var handleSwipe = function(elem,callbackOnRight, callbackOnLeft, callbackOnDown,
callbackOnUp) => {
elem.ontouchstart = handleTouchStart;
elem.ontouchend = handleTouchEnd;
var xDown = null;
var yDown = null;
function getTouches(evt) {
return evt.touches || // browser API
evt.originalEvent.touches; // jQuery
}
function handleTouchStart(evt) {
const firstTouch = getTouches(evt)[0];
xDown = firstTouch.clientX;
yDown = firstTouch.clientY;
};
function handleTouchEnd(evt) {
if (!xDown || !yDown) {
return;
}
var xUp = evt.changedTouches[0].clientX;
var yUp = evt.changedTouches[0].clientY;
var xDiff = xDown - xUp;
var yDiff = yDown - yUp;
var minDif = 30;
console.log(`xDiff:${xDiff}, yDiff:${yDiff}`);
if (Math.abs(xDiff) > Math.abs(yDiff)) {
if (xDiff > minDif) {
if (callbackOnLeft)
callbackOnLeft();
} else if (xDiff < -1 * minDif){
if (callbackOnRight)
callbackOnRight();
}
} else {
if (yDiff > minDif) {
if (callbackOnDown)
callbackOnDown();
} else if (yDiff < -1* minDif){
if (callbackOnUp)
callbackOnUp();
}
}
xDown = null;
yDown = null;
};
}
You can listen to touchstart
and touchend
events and compute the direction and force based on the event data (Codepen):
let start = null;
document.addEventListener('touchstart', e => {
const touch = e.changedTouches[0];
start = [touch.clientX, touch.clientY];
});
document.addEventListener('touchend', e => {
const touch = e.changedTouches[0];
const end = [touch.clientX, touch.clientY];
document.body.innerText = `${end[0] - start[0]},${end[1] - start[1]}`;
});
Swipe here
Or you can build a more ergonomic API around this same concept (Codepen):
const removeListener = addSwipeRightListener(document, (force, e) => {
console.info('Swiped right with force: ' + force);
});
// removeListener()
// swipe.js
const {
addSwipeLeftListener,
addSwipeRightListener,
addSwipeUpListener,
addSwipeDownListener,
} = (function() {
// <element, {listeners: [...], handleTouchstart, handleTouchend}>
const elements = new WeakMap();
function readTouch(e) {
const touch = e.changedTouches[0];
if (touch == undefined) {
return null;
}
return [touch.clientX, touch.clientY];
}
function addListener(element, cb) {
let elementValues = elements.get(element);
if (elementValues === undefined) {
const listeners = new Set();
const handleTouchstart = e => {
elementValues.start = readTouch(e);
};
const handleTouchend = e => {
const start = elementValues.start;
if (start === null) {
return;
}
const end = readTouch(e);
for (const listener of listeners) {
listener([end[0] - start[0], end[1] - start[1]], e);
}
};
element.addEventListener('touchstart', handleTouchstart);
element.addEventListener('touchend', handleTouchend);
elementValues = {
start: null,
listeners,
handleTouchstart,
handleTouchend,
};
elements.set(element, elementValues);
}
elementValues.listeners.add(cb);
return () => deleteListener(element, cb);
}
function deleteListener(element, cb) {
const elementValues = elements.get(element);
const listeners = elementValues.listeners;
listeners.delete(cb);
if (listeners.size === 0) {
elements.delete(element);
element.removeEventListener('touchstart', elementValues.handleTouchstart);
element.removeEventListener('touchend', elementValues.handleTouchend);
}
}
function addSwipeLeftListener(element, cb) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (x < 0 && -x > Math.abs(y)) {
cb(x, e);
}
});
}
function addSwipeRightListener(element, cb) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (x > 0 && x > Math.abs(y)) {
cb(x, e);
}
});
}
function addSwipeUpListener(element, cb) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (y < 0 && -y > Math.abs(x)) {
cb(x, e);
}
});
}
function addSwipeDownListener(element, cb) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (y > 0 && y > Math.abs(x)) {
cb(x, e);
}
});
}
return {
addSwipeLeftListener,
addSwipeRightListener,
addSwipeUpListener,
addSwipeDownListener,
}
})();
// app.js
function print(direction, force) {
document.querySelector('#direction').innerText = direction;
document.querySelector('#data').innerText = force;
}
addSwipeLeftListener(document, (force, e) => {
print('left', force);
});
addSwipeRightListener(document, (force, e) => {
print('right', force);
});
addSwipeUpListener(document, (force, e) => {
print('up', force);
});
addSwipeDownListener(document, (force, e) => {
print('down', force);
});
<h1>Swipe <span id="direction"></span></h1>
Force (px): <span id="data"></span>
This is JQuery
. Ideally you want to skip the swipe action if the swipe wasn't significant enough.
$('.slider')
.off('touchstart touchend swipedaction')
.on('touchstart', function (e) {
//Set the starting point directly on self
$(this).touchstartX = e.changedTouches[0].screenX;
})
.on('touchend', function (e) {
//Set the end point directly on self
let $self = $(this);
$self.touchendX = e.changedTouches[0].screenX;
// Swipe more than 50px, else don't action it.
if (Math.abs($self.touchendX - $self.touchstartX) > 50) {
if ($self.touchendX < $self.touchstartX) {
$self.trigger('swipedaction', ['left']);
} else {
$self.trigger('swipedaction', ['right']);
}
} else {
e.stopPropagation();
}
})
.on('swipedaction', function(e, direction) {
if (direction === 'left') {
// Swiped left, move right
} else {
// Swiped right, move left
}
});
e.changedTouches[0]
, but changing it to e.originalEvent.changedTouches[0]
worked (stackoverflow.com/a/46790285/6247322). Also, touchstartX
does not get stored here $(this).touchstartX = e.changedTouches[0].screenX;
. I had to create a variable outside of the listener (i.e. let $self
), then within .on('touchstart', function(e) {
I set the variable $self = $(this); $self.touchStartX = e.originalEvent.changedTouches[0].screenX;
$(this)
and $self
with the selector $('.slider')
as well. It stores the variables on the JQuery instance of the element. WIth regards to the undefined error you are getting, it is probably the best then to check for both e.changedTouches
and e.originalEvent.changedTouches
before using it.
Function checks both horizontal and vertical direction to determine which swipe was longer to prevent executing 2 instructions, because it's impossible to make a perfect one-directioned swipe. A swipe always has deviation on X and Y.
let touchstartX = 0;
let touchendX = 0;
let touchstartY = 0;
let touchendY = 0;
function checkDirection() {
let difX = touchstartX - touchendX;
let difY = touchstartY - touchendY;
if (Math.abs(difX) > Math.abs(difY)) {
if (touchendX < touchstartX) {/*left*/}
if (touchendX > touchstartX) {/*right*/}
} else {
if (touchendY < touchstartY) {/*up*/}
if (touchendY > touchstartY) {/*down*/}
}
};
document.addEventListener('touchstart', e => {
e.preventDefault();
touchstartX = e.changedTouches[0].screenX;
touchstartY = e.changedTouches[0].screenY;
});
document.addEventListener('touchend', e => {
e.preventDefault();
touchendX = e.changedTouches[0].screenX;
touchendY = e.changedTouches[0].screenY;
checkDirection();
});
class Carousel {
constructor(carouselWrapper, carouselItems, carouselPrev, carouselNext, dotContainer) {
this.carouselWrapper = document.querySelectorAll(carouselWrapper);
this.carouselItems = Array.from(document.querySelectorAll(carouselItems));
this.carouselPrev = document.querySelector(carouselPrev);
this.carouselNext = document.querySelector(carouselNext);
this.dotContainer = document.querySelector(dotContainer);
this.currentItem = 0;
this.maxItem = this.carouselItems.length;
this.isDragging = false;
this.startPos = 0;
this.currentTranslate = 0;
this.prevTranslate = 0;
this.#init();
}
#init() {
document.addEventListener('keydown', this.#keyBoardHandler.bind(this));
this.carouselPrev.addEventListener('click', this.#prevSlide.bind(this));
this.carouselNext.addEventListener('click', this.#nextSlide.bind(this));
this.dotContainer.addEventListener('click', this.#dotHandler.bind(this))
this.#createDots();
this.#gotoSlide(0)
this.#activeDots(0)
this.#touchHandler();
this.#disableoncontextmenu();
}
#touchHandler() {
this.carouselItems.forEach((slide, index) => {
const img = slide.querySelector('.carousel__bgimg');
if(!img) return;
img.addEventListener('dragstart', (e) => e.preventDefault());
slide.addEventListener('touchstart', this.#touchStart.bind(this));
slide.addEventListener('touchend', this.#touchEnd.bind(this));
slide.addEventListener('touchmove', this.#touchMove.bind(this));
});
}
#touchStart() {
this.isDragging = true;
this.startPos = this.#getpositionX(event);
}
#touchMove() {
if(this.isDragging) {
const currentPosition = this.#getpositionX(event);
this.currentTranslate = this.prevTranslate + currentPosition - this.startPos;
}
}
#touchEnd() {
this.isDragging = false;
const movedBy = this.currentTranslate - this.prevTranslate;
if(movedBy < -100) {
this.#nextSlide();
};
if(movedBy > 100) {
this.#prevSlide();
};
}
#getpositionX(event) {
return event.type.includes('mouse') ? event.pageX : event.touches[0].clientX;
}
#createDots() {
this.carouselItems.forEach((_, i) => {
this.dotContainer.insertAdjacentHTML('beforeend', `<div class="bullet" data-slide="${i}"></div>`)
});
}
#activeDots(slide) {
document.querySelectorAll('.bullet').forEach(function(dot) {
dot.classList.remove('active');
});
document.querySelector(`.bullet[data-slide="${slide}"]`)
.classList.add('active');
}
#gotoSlide(slide) {
this.carouselWrapper.forEach((s, i) => {
s.style.transform = `translate3d(${100 * (i - slide)}%, 0px, 0px)`;
});
}
#prevSlide() {
if(this.currentItem === 0) {
this.currentItem = this.maxItem - 1;
} else {
this.currentItem--;
};
this.#gotoSlide(this.currentItem);
this.#activeDots(this.currentItem);
}
#nextSlide() {
if(this.currentItem === this.maxItem -1) {
this.currentItem = 0;
} else {
this.currentItem++;
};
this.#gotoSlide(this.currentItem);
this.#activeDots(this.currentItem);
}
#dotHandler(e) {
if(e.target.classList.contains('bullet')) {
const { slide } = e.target.dataset;
this.#gotoSlide(slide);
this.#activeDots(slide);
}
}
#keyBoardHandler(e) {
if(e.keyCode === 39) this.#nextSlide();
e.keyCode === 37 && this.#prevSlide();
}
#disableoncontextmenu() {
this.carouselWrapper.forEach(function(item) {
item.oncontextmenu = function(event) {
event.preventDefault()
event.stopPropagation()
return false
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const slider = new Carousel(
'.carousel__wrapper',
'.carousel__item',
'.prev-ctrl',
'.next-ctrl',
'.dots',
);
});