How to handle touch events in Vue
Touch event handling is essential for creating mobile-friendly web applications with gestures like swipe, pinch, and tap. With over 12 years of Vue.js experience since 2014 and as the creator of CoreUI, I’ve built mobile-responsive dashboards with touch interactions. Vue provides touch event listeners that capture touch points, enabling gesture recognition and mobile-optimized user experiences. This approach creates apps that work seamlessly on touch devices with swipe navigation, pinch zoom, and tap interactions.
Use Vue touch event listeners to handle mobile gestures including swipe, pinch, and multi-touch interactions.
<template>
<div>
<!-- Basic touch events -->
<div
@touchstart='handleTouchStart'
@touchmove='handleTouchMove'
@touchend='handleTouchEnd'
@touchcancel='handleTouchCancel'
class='touch-area'
>
Touch me
<div>Touches: {{ touchCount }}</div>
</div>
<!-- Swipe detection -->
<div
@touchstart='swipeStart'
@touchend='swipeEnd'
class='swipe-area'
>
<p>{{ swipeMessage }}</p>
<p>Swipe left, right, up, or down</p>
</div>
<!-- Swipeable carousel -->
<div class='carousel'>
<div
@touchstart='carouselTouchStart'
@touchmove='carouselTouchMove'
@touchend='carouselTouchEnd'
class='carousel-container'
>
<div
v-for='(slide, index) in slides'
:key='index'
:class="{ active: currentSlide === index }"
class='slide'
>
{{ slide }}
</div>
</div>
<div class='indicators'>
<span
v-for='(slide, index) in slides'
:key='index'
:class="{ active: currentSlide === index }"
class='indicator'
/>
</div>
</div>
<!-- Pinch zoom -->
<div
@touchstart='pinchStart'
@touchmove='pinchMove'
@touchend='pinchEnd'
class='pinch-area'
>
<div
:style='{ transform: `scale(${scale})` }'
class='pinch-content'
>
Pinch to zoom
<p>Scale: {{ scale.toFixed(2) }}x</p>
</div>
</div>
<!-- Long press detection -->
<div
@touchstart='longPressStart'
@touchend='longPressEnd'
@touchmove='longPressCancel'
:class="{ pressed: isLongPressed }"
class='long-press-area'
>
{{ isLongPressed ? 'Long press detected!' : 'Press and hold' }}
</div>
<!-- Tap vs Long Press -->
<div
@touchstart='tapStart'
@touchend='tapEnd'
class='tap-area'
>
{{ tapMessage }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const touchCount = ref(0)
const swipeMessage = ref('Swipe to detect direction')
const touchStartX = ref(0)
const touchStartY = ref(0)
const touchStartTime = ref(0)
const handleTouchStart = (event) => {
touchCount.value = event.touches.length
console.log('Touch start:', event.touches.length, 'touches')
}
const handleTouchMove = (event) => {
console.log('Touch move')
}
const handleTouchEnd = () => {
console.log('Touch end')
touchCount.value = 0
}
const handleTouchCancel = () => {
console.log('Touch cancelled')
}
const swipeStart = (event) => {
touchStartX.value = event.touches[0].clientX
touchStartY.value = event.touches[0].clientY
touchStartTime.value = Date.now()
}
const swipeEnd = (event) => {
const touchEndX = event.changedTouches[0].clientX
const touchEndY = event.changedTouches[0].clientY
const diffX = touchEndX - touchStartX.value
const diffY = touchEndY - touchStartY.value
const diffTime = Date.now() - touchStartTime.value
if (diffTime > 1000) return
if (Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 50) {
swipeMessage.value = 'Swiped RIGHT →'
} else if (diffX < -50) {
swipeMessage.value = 'Swiped LEFT ←'
}
} else {
if (diffY > 50) {
swipeMessage.value = 'Swiped DOWN ↓'
} else if (diffY < -50) {
swipeMessage.value = 'Swiped UP ↑'
}
}
}
const slides = ['Slide 1', 'Slide 2', 'Slide 3', 'Slide 4']
const currentSlide = ref(0)
const carouselStartX = ref(0)
const carouselTouchStart = (event) => {
carouselStartX.value = event.touches[0].clientX
}
const carouselTouchMove = (event) => {
event.preventDefault()
}
const carouselTouchEnd = (event) => {
const touchEndX = event.changedTouches[0].clientX
const diff = touchEndX - carouselStartX.value
if (diff > 50 && currentSlide.value > 0) {
currentSlide.value--
} else if (diff < -50 && currentSlide.value < slides.length - 1) {
currentSlide.value++
}
}
const scale = ref(1)
const initialDistance = ref(0)
const pinchStart = (event) => {
if (event.touches.length === 2) {
const touch1 = event.touches[0]
const touch2 = event.touches[1]
initialDistance.value = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
)
}
}
const pinchMove = (event) => {
if (event.touches.length === 2) {
event.preventDefault()
const touch1 = event.touches[0]
const touch2 = event.touches[1]
const currentDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
)
scale.value = Math.max(0.5, Math.min(3, currentDistance / initialDistance.value))
}
}
const pinchEnd = () => {
initialDistance.value = 0
}
const isLongPressed = ref(false)
let longPressTimer = null
const longPressStart = () => {
longPressTimer = setTimeout(() => {
isLongPressed.value = true
}, 500)
}
const longPressEnd = () => {
clearTimeout(longPressTimer)
setTimeout(() => {
isLongPressed.value = false
}, 500)
}
const longPressCancel = () => {
clearTimeout(longPressTimer)
}
const tapMessage = ref('Tap or press and hold')
let tapTimer = null
const tapStart = () => {
tapTimer = setTimeout(() => {
tapMessage.value = 'Long press detected!'
}, 500)
}
const tapEnd = () => {
const elapsed = Date.now() - touchStartTime.value
clearTimeout(tapTimer)
if (elapsed < 500) {
tapMessage.value = 'Tap detected!'
}
setTimeout(() => {
tapMessage.value = 'Tap or press and hold'
}, 1000)
}
</script>
<style scoped>
.touch-area,
.swipe-area,
.pinch-area,
.long-press-area,
.tap-area {
padding: 40px;
border: 2px solid #ddd;
margin: 10px 0;
text-align: center;
user-select: none;
}
.carousel {
margin: 20px 0;
}
.carousel-container {
overflow: hidden;
border: 2px solid #ddd;
height: 200px;
position: relative;
}
.slide {
display: none;
align-items: center;
justify-content: center;
height: 100%;
font-size: 24px;
}
.slide.active {
display: flex;
}
.indicators {
text-align: center;
padding: 10px;
}
.indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #ddd;
margin: 0 5px;
}
.indicator.active {
background: #007bff;
}
.pinch-area {
height: 300px;
overflow: hidden;
touch-action: none;
}
.pinch-content {
transition: transform 0.1s;
}
.long-press-area {
transition: background 0.3s;
}
.long-press-area.pressed {
background: #007bff;
color: white;
}
</style>
Best Practice Note
Use event.preventDefault() in touch move handlers to prevent default scrolling behavior. Calculate swipe direction using touch coordinates—minimum 50px movement to avoid accidental triggers. For pinch zoom, use Math.hypot() to calculate distance between two touch points. Implement timing checks to differentiate between tap and long press (typically 500ms threshold). Use touch-action: none CSS property to disable browser gesture handling. This is how we implement touch interactions in CoreUI for Vue—providing mobile-optimized gestures for dashboards and admin panels that work seamlessly on tablets and smartphones with natural touch navigation.



