JavaScript and TypeScript course Β· Module 8: Events and the DOM
Advanced Event Handling Techniques
Specialized techniques for advanced use cases.
More and more park guests browse the dinosaur gallery on phones and tablets. There's no mouse there, and a click event alone won't tell you which way someone swiped. Instead, the browser reports three touch events in a row: touchstart when a finger touches the screen, touchmove when it moves across it, and touchend when it leaves it. From these three signals we'll build gesture recognition, just as a tracker reads the direction of a herd from footprints in the mud.
Touch Gesture Handling
All the logic will live in the TouchGestureHandler class. The constructor remembers the element and prepares room for the coordinates of the start and the current finger position.
// Touch gesture handling for mobile devices
class TouchGestureHandler {
constructor(element) {
this.element = element;
this.startX = 0;
this.startY = 0;
this.currentX = 0;
this.currentY = 0;
this.isTracking = false;
this.bindEvents();
}
bindEvents() {
this.element.addEventListener('touchstart', (e) => this.handleTouchStart(e));
this.element.addEventListener('touchmove', (e) => this.handleTouchMove(e));
this.element.addEventListener('touchend', (e) => this.handleTouchEnd(e));
}The bindEvents() method attaches three listeners, and the arrow functions keep this pointing at the class instance. The isTracking flag says whether a gesture is in progress. The first signal is touching the screen.
handleTouchStart(e) {
this.isTracking = true;
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
this.currentX = this.startX; // a tap without movement = zero delta
this.currentY = this.startY;
}The e.touches property is a list of all the fingers currently touching the screen, and touches[0] is the first of them. Its clientX and clientY work just like with a mouse. The last two lines are a fix: they reset the movement at the start, so a simple tap without any movement won't compare the start with the position from the previous gesture and won't trigger a phantom swipe. Now the finger's movement.
handleTouchMove(e) {
if (!this.isTracking) return;
e.preventDefault();
this.currentX = e.touches[0].clientX;
this.currentY = e.touches[0].clientY;
const deltaX = this.currentX - this.startX;
const deltaY = this.currentY - this.startY;
// Emit custom event with gesture data
this.element.dispatchEvent(new CustomEvent('gesture', {
detail: { deltaX, deltaY, isActive: true }
}));
}Calling preventDefault() blocks page scrolling during the gesture, but it only works in a listener that isn't passive. That's not a problem here, because we listen on a regular element. On window, document, document.documentElement, and body, browsers treat touchstart and touchmove as passive by default and will ignore such a call. During the movement, the class emits its own gesture event with the offset in detail, so a card can follow the finger in real time. Finally, the finger leaves the screen.
handleTouchEnd(e) {
if (!this.isTracking) return;
this.isTracking = false;
const deltaX = this.currentX - this.startX;
const deltaY = this.currentY - this.startY;
// Determine gesture type
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal gesture
if (deltaX > 50) {
this.element.dispatchEvent(new CustomEvent('swipeRight'));
} else if (deltaX < -50) {
this.element.dispatchEvent(new CustomEvent('swipeLeft'));
}
} else {
// Vertical gesture
if (deltaY > 50) {
this.element.dispatchEvent(new CustomEvent('swipeDown'));
} else if (deltaY < -50) {
this.element.dispatchEvent(new CustomEvent('swipeUp'));
}
}
this.element.dispatchEvent(new CustomEvent('gestureEnd', {
detail: { deltaX, deltaY }
}));
}
}In touchend, the e.touches list no longer contains the lifted finger, which ends up in e.changedTouches, so we use the saved currentX and currentY. The Math.abs() comparison determines the dominant axis of movement. The 50-pixel threshold tells a deliberate gesture from a trembling finger. The Y axis on the screen grows downward, so a positive deltaY means a swipe down. The whole sequence always looks the same: touchstart, a series of touchmove events, touchend, and finally the analysis of deltaX and deltaY and the emission of a custom event. There is also a fourth event, touchcancel, which arrives when the system interrupts the touch, for example with too many fingers at once. In its handler it's enough to reset the isTracking flag.
Using It in the Dinosaur Gallery
The class knows nothing about the gallery, it only broadcasts events. The gallery listens to the ones it cares about.
const dinoGallery = document.getElementById('dino-gallery');
new TouchGestureHandler(dinoGallery);
dinoGallery.addEventListener('swipeLeft', () => showNextDinosaur());
dinoGallery.addEventListener('swipeRight', () => showPreviousDinosaur());A swipe to the left shows the next dinosaur, and a swipe to the right the previous one. Remember that a CustomEvent doesn't bubble by default, so the listeners have to sit on the same element we passed to the handler. You met exactly this division of roles with custom events: one module detects, another reacts.
My advice: if you're building a component from scratch that should work with both a mouse and a finger, check out Pointer Events (pointerdown, pointermove, pointerup). They handle the mouse, pen, and touch with a single set of events. Touch Events are still useful where you need a list of all fingers, for example for a pinch gesture. In the sandbox below you'll test gestures on a touch area, and on a computer with the mouse as well. In the next lesson we'll deal with performance, including passive listeners.
Remember: a gesture is a footprint in the mud that you read only once the dinosaur lifts its foot, that is, in touchend.
Code for this lesson: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Touch Gestures</title>
<style>
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; touch-action: none; }
.touch-area { width: 100%; height: 300px; background: rgba(52, 152, 219, 0.2); border: 2px dashed #3498db; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 24px; margin: 20px 0; }
.info { background: rgba(0,0,0,0.6); padding: 15px; border-radius: 8px; margin: 10px 0; }
</style>
</head>
<body>
<h1>Touch Gestures - Touch Control</h1>
<div class="info">
<p>Swipe: ββ (horizontal) or ββ (vertical)</p>
<p id="gesture-info">Waiting for a gesture...</p>
</div>
<div class="touch-area" id="touch-area">Touch and drag</div>
<div class="info">
<strong>Detected gestures:</strong>
<ul id="gesture-log"></ul>
</div>
<script>
console.log('Touch Gesture Handler initialized');
const touchArea = document.getElementById('touch-area');
const gestureInfo = document.getElementById('gesture-info');
const gestureLog = document.getElementById('gesture-log');
let startX = 0, startY = 0;
let currentX = 0, currentY = 0;
let isTracking = false;
function logGesture(message) {
const li = document.createElement('li');
li.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
gestureLog.insertBefore(li, gestureLog.firstChild);
if (gestureLog.children.length > 5) {
gestureLog.removeChild(gestureLog.lastChild);
}
}
touchArea.addEventListener('touchstart', (e) => {
isTracking = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
currentX = startX;
currentY = startY;
gestureInfo.textContent = 'Gesture started...';
touchArea.style.background = 'rgba(46, 204, 113, 0.2)';
console.log('Touch start:', startX, startY);
});
touchArea.addEventListener('touchmove', (e) => {
if (!isTracking) return;
e.preventDefault();
currentX = e.touches[0].clientX;
currentY = e.touches[0].clientY;
const deltaX = currentX - startX;
const deltaY = currentY - startY;
gestureInfo.textContent = `Ξ X: ${Math.round(deltaX)}px, Y: ${Math.round(deltaY)}px`;
});
touchArea.addEventListener('touchend', (e) => {
if (!isTracking) return;
isTracking = false;
const deltaX = currentX - startX;
const deltaY = currentY - startY;
touchArea.style.background = 'rgba(52, 152, 219, 0.2)';
let gesture = 'Unrecognized';
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX > 50) {
gesture = 'β Swipe Right';
touchArea.textContent = 'β Right';
} else if (deltaX < -50) {
gesture = 'β Swipe Left';
touchArea.textContent = 'β Left';
}
} else {
if (deltaY > 50) {
gesture = 'β Swipe Down';
touchArea.textContent = 'β Down';
} else if (deltaY < -50) {
gesture = 'β Swipe Up';
touchArea.textContent = 'β Up';
}
}
logGesture(gesture);
console.log(`Gesture detected: ${gesture}`);
setTimeout(() => {
touchArea.textContent = 'Touch and drag';
gestureInfo.textContent = 'Waiting for the next gesture...';
}, 1500);
});
// Mouse fallback (for testing on desktop)
let mouseDown = false;
touchArea.addEventListener('mousedown', (e) => {
mouseDown = true;
startX = e.clientX;
startY = e.clientY;
currentX = startX;
currentY = startY;
});
touchArea.addEventListener('mousemove', (e) => {
if (!mouseDown) return;
currentX = e.clientX;
currentY = e.clientY;
const deltaX = currentX - startX;
const deltaY = currentY - startY;
gestureInfo.textContent = `Mouse Ξ X: ${Math.round(deltaX)}px, Y: ${Math.round(deltaY)}px`;
});
touchArea.addEventListener('mouseup', () => {
if (mouseDown) {
mouseDown = false;
const deltaX = currentX - startX;
const deltaY = currentY - startY;
if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
logGesture('Mouse gesture detected');
}
}
});
console.log('Touch gestures ready - swipe to test!');
</script>
</body>
</html>Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which event is triggered when the user swipes their finger across a touchscreen?
Hands-on tasks in the game
- Code editor
The index.js file has the TouchGestureHandler class from the gestures lesson. Fill in the blanks: ___BLANK1___ is the touch event sent while the finger moves across the screen, ___BLANK2___ is the name of the direction when the finger moved up by more than 50 px, and ___BLANK3___ is the CustomEvent option field in which the swipe event carries the data { direction, deltaX, deltaY }. A move of more than 50 px should send one swipe event with the direction left, right, up or down, and a tap or a small move none.
- Vertical ordering
Arrange touch events in the order they occur during a finger swipe across the screen: