Touch Events — Complete Guide
In this tutorial, you will learn about Touch Events. We cover key concepts, practical examples, and best practices to help you master this topic.
Touch events (touchstart, touchmove, touchend) enable JavaScript to handle multi-touch interactions on mobile devices and touch-enabled screens for gestures and input.
What You'll Learn
- The touch event lifecycle: touchstart, touchmove, touchend, touchcancel
- How to access touch points through event.touches, event.targetTouches, event.changedTouches
- How to implement common gestures: tap, swipe, pinch, rotate
- How to handle both mouse and touch events together
Why It Matters
Over half of web traffic comes from mobile devices. Touch events power swipe navigation, pinch-to-zoom, pull-to-refresh, drag-and-drop, and drawing on touch screens. Supporting touch is essential for modern web applications.
Real-World Use
- A photo gallery supports swipe gestures to navigate between images
- A map application supports pinch-to-zoom and two-finger rotate
- A mobile game uses multi-touch for dual-stick controls
- A drawing app captures touch strokes with pressure sensitivity
flowchart LR A[Touch on Screen] --> B[touchstart] B --> C[Touch Point Created] C --> D[touchmove] D --> E[Touch Point Moves] E --> D D --> F[touchend] E --> G[touchcancel] G --> H[System Interrupts] F --> I[Gesture Recognized]
Basic Touch Events
The three primary touch events mirror mouse events but support multiple simultaneous touch points.
const touchArea = document.querySelector('.touch-area');
const touchLog = document.querySelector('.touch-log');
touchArea.addEventListener('touchstart', function(event) {
// Prevent default to avoid scroll interference
event.preventDefault();
console.log('Touch started');
console.log('Number of touches:', event.touches.length);
console.log('Target touches:', event.targetTouches.length);
console.log('Changed touches:', event.changedTouches.length);
// Get the first touch point
const touch = event.touches[0];
console.log('Position:', touch.clientX, touch.clientY);
console.log('Touch ID:', touch.identifier);
touchLog.textContent = `Touch start at (${Math.round(touch.clientX)}, ${Math.round(touch.clientY)})`;
});
touchArea.addEventListener('touchmove', function(event) {
event.preventDefault();
const touch = event.touches[0];
touchLog.textContent = `Touch move at (${Math.round(touch.clientX)}, ${Math.round(touch.clientY)})`;
});
touchArea.addEventListener('touchend', function(event) {
// In touchend, event.touches may be empty
const touch = event.changedTouches[0];
console.log('Touch ended at:', touch.clientX, touch.clientY);
touchLog.textContent = `Touch ended at (${Math.round(touch.clientX)}, ${Math.round(touch.clientY)})`;
});
Expected output: Touching the area logs the start position. Moving the finger updates the log. Lifting the finger logs the end position. The event.touches array contains all active touch points.
Touch Lists
Three touch lists provide different views of the touch state.
touchArea.addEventListener('touchstart', function(event) {
// event.touches: ALL touch points currently on the screen
// (including those on other elements)
// event.targetTouches: touch points ON this element
// (subset of touches)
// event.changedTouches: touch points that changed
// in this event (started, moved, or ended)
console.log('All touches:', event.touches.length);
console.log('On this element:', event.targetTouches.length);
console.log('Changed:', event.changedTouches.length);
// For touchstart: changedTouches === touches
// For touchend: changedTouches has the removed touch, touches is empty
// For touchmove: changedTouches has moved touches
// Iterate all touch points
for (let i = 0; i < event.touches.length; i++) {
const touch = event.touches[i];
console.log(`Touch ${touch.identifier}: (${touch.clientX}, ${touch.clientY})`);
}
});
Expected output: The console shows counts for each touch list. On touchstart, all three lists have the same length. On touchend, touches may be empty while changedTouches has the released touch.
Implementing Gestures
Common gestures can be detected by tracking touch positions over time.
const gestureArea = document.querySelector('.gesture-area');
let startX, startY, startTime;
let lastTap = 0;
gestureArea.addEventListener('touchstart', function(event) {
const touch = event.touches[0];
startX = touch.clientX;
startY = touch.clientY;
startTime = Date.now();
});
gestureArea.addEventListener('touchend', function(event) {
const touch = event.changedTouches[0];
const endX = touch.clientX;
const endY = touch.clientY;
const deltaX = endX - startX;
const deltaY = endY - startY;
const elapsed = Date.now() - startTime;
// Detect tap (quick touch without much movement)
if (Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10 && elapsed < 200) {
console.log('Tap detected');
handleTap(endX, endY);
}
// Detect swipe (fast movement in one direction)
if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
console.log(deltaX > 0 ? 'Swipe Right' : 'Swipe Left');
} else {
console.log(deltaY > 0 ? 'Swipe Down' : 'Swipe Up');
}
}
// Detect long press
if (Math.abs(deltaX) < 20 && Math.abs(deltaY) < 20 && elapsed > 500) {
console.log('Long press detected');
handleLongPress(endX, endY);
}
// Detect double tap
const now = Date.now();
if (now - lastTap < 300) {
console.log('Double tap detected');
handleDoubleTap(endX, endY);
}
lastTap = now;
});
Expected output: Tapping shows "Tap detected". Swiping identifies the direction. Holding without moving shows "Long press detected". Two quick taps show "Double tap detected".
Multi-Touch: Pinch and Rotate
Track two touch points to detect pinch-to-zoom and rotation gestures.
const pinchArea = document.querySelector('.pinch-area');
let initialDistance = 0;
let initialAngle = 0;
let currentScale = 1;
let currentRotation = 0;
pinchArea.addEventListener('touchstart', function(event) {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
initialDistance = getDistance(touch1, touch2);
initialAngle = getAngle(touch1, touch2);
console.log('Pinch start, distance:', initialDistance);
}
});
pinchArea.addEventListener('touchmove', function(event) {
event.preventDefault();
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = getDistance(touch1, touch2);
const angle = getAngle(touch1, touch2);
// Scale
const scale = distance / initialDistance;
currentScale = Math.max(0.5, Math.min(3, scale));
console.log('Scale:', currentScale.toFixed(2));
// Rotation
const rotation = angle - initialAngle;
currentRotation = rotation;
// Apply transform
pinchArea.style.transform = `
scale(${currentScale})
rotate(${currentRotation}deg)
`;
}
});
function getDistance(t1, t2) {
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function getAngle(t1, t2) {
const dx = t2.clientX - t1.clientX;
const dy = t2.clientY - t1.clientY;
return Math.atan2(dy, dx) * 180 / Math.PI;
}
Expected output: Pinching with two fingers scales the element up or down. Rotating two fingers rotates the element. The scale is clamped between 0.5 and 3.
Handling Both Mouse and Touch
Mobile browsers fire both touch and mouse events. Prevent duplicate handling by tracking the input type.
const interactive = document.querySelector('.interactive');
let isTouchDevice = false;
// Detect touch capability
interactive.addEventListener('touchstart', function(event) {
isTouchDevice = true;
console.log('Touch interaction');
handleStart(event.touches[0].clientX, event.touches[0].clientY);
}, { passive: true });
interactive.addEventListener('touchend', function(event) {
if (isTouchDevice) {
handleEnd(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
// Prevent the subsequent mouse event
event.preventDefault();
}
});
// Mouse fallback for non-touch devices
interactive.addEventListener('mousedown', function(event) {
if (!isTouchDevice) {
console.log('Mouse interaction');
handleStart(event.clientX, event.clientY);
}
});
interactive.addEventListener('mouseup', function(event) {
if (!isTouchDevice) {
handleEnd(event.clientX, event.clientY);
}
});
function handleStart(x, y) {
console.log('Interaction started at:', x, y);
}
function handleEnd(x, y) {
console.log('Interaction ended at:', x, y);
}
// Alternative: use Pointer Events API (unified mouse/touch/stylus)
// pointerdown, pointermove, pointerup work for all input types
Expected output: On touch devices, only touch events fire (mouse events are suppressed). On mouse devices, mouse events fire. Both paths call the same handler functions.
Common Mistakes
- Not calling preventDefault on touchstart — Without preventDefault, the browser may interpret the touch as a scroll or gesture, causing conflicts with custom interactions.
- Using event.touches in touchend — In touchend, event.touches no longer contains the released touch. Use event.changedTouches instead.
- Forgetting to handle touchcancel — touchcancel fires when the system interrupts the touch (incoming call, alert, gesture conflict). Always clean up state in touchcancel.
- Not accounting for multi-touch in single-touch handlers — If only tracking event.touches[0], you may get the wrong finger when multiple fingers are on screen.
- Ignoring 300ms tap delay — Modern browsers remove the delay when viewport meta tag is present. If not, use touch-action: manipulation CSS property.
Practice Questions
- What are the three touch lists and when should you use each? touches (all active touches), targetTouches (touches on this element), changedTouches (touches that changed). Use changedTouches in touchend/touchcancel.
- How do you prevent the browser from handling a touch as a scroll? Call event.preventDefault() on the touchstart or touchmove event.
- How do you detect a swipe gesture? Compare the starting and ending touch positions. If the distance exceeds a threshold (50px) and occurs within a time limit (300ms), it is a swipe.
- Challenge: Implement pull-to-refresh. Detecting a downward swipe at the top of the page triggers a refresh animation. When the pull exceeds a threshold, fire a refresh callback. Use CSS transitions for the snap-back animation.
FAQ
Mini Project
Build an image gallery that supports swipe navigation. Display one image at a time. Swiping left shows the next image, swiping right shows the previous. Add a smooth CSS transition for the slide animation. Show dot indicators below the image indicating the current position. Handle touchcancel by snapping back to the current image without changing the index.
What's Next
Continue with Lesson 20: Intersection Observer to learn how to detect when elements become visible in the viewport for Lazy Loading and animations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro