Touch Gesten in Vue Apps: Swipe, Drag und Pinch implementieren
AI generated
<v/>
{ }
Vue.js · Touch Gesten · Mobile UX · Pointer Events
Touch Gesten in Vue Apps richtig implementieren
Swipe, Drag und Pinch ohne Ruckeln und ohne Scroll-Konflikte

Touch Gesten in Vue Apps scheitern selten an der Grundidee, sondern an Details wie Scroll-Konflikten, fehlender Passive-Event-Konfiguration und ruckelnden Animationen unter Last. Mit Pointer Events, VueUse-Composables und sauberer Trennung von Erkennung und Reaktion lassen sich Swipe-to-Delete, Drag-and-Drop und Pinch-to-Zoom robust und performant umsetzen.

18 Min. Lesezeit Vue 3 · Pointer Events · VueUse Swipe · Drag · Pinch-to-Zoom

1. Warum Touch Gesten mehr sind als Click-Handler

Touch Gesten in Vue Apps unterscheiden sich fundamental von einfachen Klick-Interaktionen, weil sie über die Zeit ablaufen und mehrere Zustände durchlaufen: Start der Berührung, Bewegung mit Richtung und Geschwindigkeit, und Ende mit einer Entscheidung, ob die Geste als abgeschlossen zählt. Ein einzelner @click-Handler kennt nur einen Moment, während eine Swipe-Geste kontinuierlich Positionsdaten liefert, aus denen erst am Ende eine Aktion abgeleitet wird.

Der zweite grundlegende Unterschied betrifft die Interaktion mit dem nativen Scroll-Verhalten des Browsers. Wer Touch Gesten in Vue Apps implementiert, konkurriert direkt mit dem Betriebssystem um dieselben Touch-Events. Ein horizontales Swipe-to-Delete-Pattern in einer vertikal scrollbaren Liste muss sauber zwischen "Nutzer will scrollen" und "Nutzer will swipen" unterscheiden, sonst entsteht entweder ein rucklendes Scroll-Verhalten oder eine Geste, die versehentlich ausgelöst wird, während der Nutzer eigentlich nur scrollen wollte.

Drittens verlangen Touch Gesten in Vue Apps eine Trennung zwischen Erkennung und Reaktion. Die Erkennung, also das Auswerten von Position, Richtung und Geschwindigkeit über die Zeit, ist unabhängig von der konkreten Aktion, die eine Komponente bei einer erkannten Geste ausführen soll. Wer diese beiden Ebenen sauber trennt, kann Gesture-Erkennungslogik als wiederverwendbares Composable bauen und in vielen unterschiedlichen Komponenten mit jeweils eigener Reaktion einsetzen.

2. Pointer Events statt Touch- und Mouse-Events getrennt

Moderne Browser unterstützen die Pointer-Events-API, die Maus-, Touch- und Stift-Eingaben unter einem einheitlichen Event-Modell zusammenfasst. Für Touch Gesten in Vue Apps bedeutet das: Statt separater Listener für touchstart/touchmove/touchend und mousedown/mousemove/mouseup reicht ein einziger Satz von pointerdown/pointermove/pointerup-Listenern, die auf jedem Eingabegerät funktionieren.

Ein wichtiges Detail ist setPointerCapture, das dafür sorgt, dass alle weiteren Pointer-Events an dasselbe Element gebunden bleiben, selbst wenn der Finger oder Cursor während der Bewegung das ursprüngliche Element verlässt. Ohne diesen Aufruf verliert eine Drag-Interaktion die Verbindung zum Zielelement, sobald der Nutzer schnell über andere DOM-Elemente hinwegzieht, was zu abgebrochenen oder inkonsistenten Gesten führt.


// composables/usePointerTracking.js
import { ref } from 'vue'

export function usePointerTracking(elementRef, onMove, onEnd) {
  const isTracking = ref(false)
  const startX = ref(0)
  const startY = ref(0)

  function handlePointerDown(event) {
    isTracking.value = true
    startX.value = event.clientX
    startY.value = event.clientY
    // Ensures this element keeps receiving events even if the pointer
    // moves outside its bounds during a fast drag
    elementRef.value.setPointerCapture(event.pointerId)
  }

  function handlePointerMove(event) {
    if (!isTracking.value) return
    const deltaX = event.clientX - startX.value
    const deltaY = event.clientY - startY.value
    onMove({ deltaX, deltaY, event })
  }

  function handlePointerUp(event) {
    if (!isTracking.value) return
    isTracking.value = false
    onEnd({ deltaX: event.clientX - startX.value, deltaY: event.clientY - startY.value })
  }

  return { handlePointerDown, handlePointerMove, handlePointerUp }
}

Für einfachere Anwendungsfälle ist es meist nicht nötig, dieses Composable komplett selbst zu bauen: VueUse liefert mit useEventListener und spezialisierten Composables wie usePointerSwipe bereits fertige, gut getestete Bausteine, die dieselbe Pointer-Events-Grundlage nutzen. Für sehr spezielle Anforderungen, etwa Mehrfinger-Gesten, bleibt eine eigene Implementierung auf Basis von Pointer Events die flexiblere Wahl.

3. Swipe-Erkennung: Richtung, Distanz und Geschwindigkeit

Eine zuverlässige Swipe-Erkennung für Touch Gesten in Vue Apps braucht mehr als nur die zurückgelegte Distanz. Ein Swipe sollte erst als abgeschlossen gelten, wenn sowohl eine Mindestdistanz überschritten als auch eine Mindestgeschwindigkeit erreicht wurde, sonst werden auch langsame, absichtslose Bewegungen fälschlich als Swipe interpretiert. VueUse bietet mit usePointerSwipe ein fertiges Composable, das genau diese Schwellenwerte konfigurierbar macht.

Für die Richtungserkennung ist außerdem wichtig, klar zwischen horizontalen und vertikalen Swipes zu unterscheiden, indem der Winkel der Bewegung ausgewertet wird. Eine Bewegung, die primär vertikal verläuft, sollte nicht versehentlich als horizontaler Swipe gewertet werden, selbst wenn eine kleine horizontale Komponente vorhanden ist. Diese Unterscheidung verhindert, dass normales vertikales Scrollen fälschlicherweise ein Swipe-to-Delete auslöst.


// components/SwipeToDelete.vue — script setup block
import { ref } from 'vue'
import { usePointerSwipe } from '@vueuse/core'

const cardRef = ref(null)
const offsetX = ref(0)
const props = defineProps({ item: { type: Object, required: true } })
const emit = defineEmits(['delete'])

const { isSwiping, direction } = usePointerSwipe(cardRef, {
  threshold: 60,          // minimum distance in pixels
  onSwipe(event) {
    // Live-follow the finger while swiping left
    if (direction.value === 'left') {
      offsetX.value = Math.min(0, event.movementX)
    }
  },
  onSwipeEnd(event, dir) {
    if (dir === 'left' && Math.abs(offsetX.value) > 100) {
      emit('delete', props.item)
    } else {
      offsetX.value = 0 // snap back if the swipe was not far enough
    }
  },
})

Der entscheidende Punkt bei diesem Muster: Der visuelle Offset folgt der Fingerbewegung in Echtzeit während onSwipe feuert, während die eigentliche Lösch-Entscheidung erst in onSwipeEnd anhand der finalen Distanz getroffen wird. Diese Trennung zwischen kontinuierlichem visuellem Feedback und abschließender Aktion ist charakteristisch für gut funktionierende Touch Gesten in Vue Apps.

4. Drag-Handling mit Composables statt Inline-Logik

Drag-Interaktionen, etwa das Verschieben einer Karte innerhalb eines Containers oder das Reordering von Listen-Elementen, folgen einem ähnlichen Muster wie Swipe, benötigen aber zusätzlich eine kontinuierliche Positionsaktualisierung während der gesamten Bewegung, nicht nur am Ende. VueUse liefert dafür useDraggable, das Position, Bewegungsstatus und Grenzen-Constraints in einem einzigen Composable kapselt.

Ein häufiger Fehler bei Touch Gesten in Vue Apps mit Drag-Funktionalität: Die Positionsberechnung erfolgt direkt im Event-Handler ohne requestAnimationFrame, wodurch bei schnellen Bewegungen mehr DOM-Updates ausgelöst werden, als der Browser rendern kann. VueUse kapselt diese Optimierung bereits intern, bei einer eigenen Implementierung sollte sie manuell ergänzt werden.


// components/DraggableCard.vue — script setup block
import { ref } from 'vue'
import { useDraggable } from '@vueuse/core'

const cardRef = ref(null)
const containerRef = ref(null)

const { x, y, isDragging } = useDraggable(cardRef, {
  initialValue: { x: 40, y: 40 },
  containerElement: containerRef,   // constrains movement to the container bounds
  onEnd(position) {
    persistPosition(position)       // e.g. save layout to localStorage or an API
  },
})

// template binds :style="{ left: x + 'px', top: y + 'px' }" to cardRef

Für Listen-Reordering per Drag empfiehlt sich zusätzlich eine visuelle Platzhalter-Logik: Während des Ziehens verschiebt sich die Zielposition der übrigen Elemente bereits sichtbar, bevor der Drag beendet wird, damit der Nutzer die zukünftige Anordnung sieht. Diese Vorschau-Logik ist unabhängig vom eigentlichen Drag-Composable und sollte separat in der Komponente implementiert werden, die die Liste rendert.

5. Pinch-to-Zoom mit zwei gleichzeitigen Pointern

Pinch-to-Zoom unterscheidet sich von Swipe und Drag dadurch, dass zwei gleichzeitige Kontaktpunkte verarbeitet werden müssen, deren Abstand zueinander die Zoom-Stufe bestimmt. Für Touch Gesten in Vue Apps mit Zoom-Funktionalität, etwa in Bildergalerien oder Karten-Komponenten, müssen beide aktiven Pointer über ihre pointerId unterschieden und ihre Positionen kontinuierlich verglichen werden.

Die Zoom-Berechnung basiert auf dem Verhältnis des aktuellen Abstands zweier Pointer zum Abstand beim Start der Geste. Wichtig ist außerdem, den Zoom-Mittelpunkt zu berechnen, damit das Bild um die Stelle zwischen den beiden Fingern zoomt, statt um den Mittelpunkt des gesamten Elements, was sich für Nutzer unnatürlich anfühlen würde.


// composables/usePinchZoom.js
import { ref } from 'vue'

export function usePinchZoom(elementRef) {
  const scale = ref(1)
  const activePointers = new Map()
  let startDistance = 0
  let startScale = 1

  function distance(p1, p2) {
    return Math.hypot(p1.clientX - p2.clientX, p1.clientY - p2.clientY)
  }

  function handlePointerDown(event) {
    activePointers.set(event.pointerId, event)
    if (activePointers.size === 2) {
      const [p1, p2] = [...activePointers.values()]
      startDistance = distance(p1, p2)
      startScale = scale.value
    }
  }

  function handlePointerMove(event) {
    if (!activePointers.has(event.pointerId)) return
    activePointers.set(event.pointerId, event)
    if (activePointers.size === 2) {
      const [p1, p2] = [...activePointers.values()]
      const currentDistance = distance(p1, p2)
      scale.value = Math.min(4, Math.max(1, startScale * (currentDistance / startDistance)))
    }
  }

  function handlePointerUp(event) {
    activePointers.delete(event.pointerId)
  }

  return { scale, handlePointerDown, handlePointerMove, handlePointerUp }
}

Diese Implementierung begrenzt den Zoom-Faktor bewusst zwischen 1 und 4, was in der Praxis für die meisten Bildbetrachter-Anwendungsfälle ausreicht und verhindert, dass Nutzer ein Bild auf ein unbrauchbares Maß verkleinern oder ins Unermessliche vergrößern. Für Touch Gesten in Vue Apps mit Kartenkomponenten empfiehlt sich stattdessen oft eine bestehende Mapping-Bibliothek, die Pinch-to-Zoom bereits eingebaut hat, statt eine eigene Implementierung zu pflegen.

6. Scroll-Konflikte lösen: touch-action und passive Listener

Das häufigste praktische Problem bei Touch Gesten in Vue Apps ist der Konflikt mit dem nativen Scroll-Verhalten des Browsers. Die CSS-Eigenschaft touch-action steuert, welche nativen Touch-Gesten ein Element überhaupt an den Browser weiterleitet. Mit touch-action: pan-y auf einem horizontal swipbaren Element erlaubt man vertikales Scrollen weiterhin nativ, während horizontale Bewegungen an die eigene JavaScript-Logik gehen, ohne dass der Browser dabei ins Gehege kommt.

Ein zweites, oft übersehenes Detail betrifft passive Event-Listener. Standardmäßig registriert Vue Touch-Event-Listener als passiv, was bedeutet, dass preventDefault() innerhalb des Handlers keine Wirkung zeigt und der Browser parallel bereits mit dem Scrollen beginnt. Für Fälle, in denen das native Scrollen tatsächlich unterdrückt werden muss, etwa während eines aktiven Drags, muss der Listener explizit als nicht-passiv registriert werden.


<!-- touch-action controls which native gestures reach the browser at all -->
<div
  class="swipe-card"
  style="touch-action: pan-y;"
  @pointerdown="handlePointerDown"
>
  <!-- pan-y: vertical scroll stays native, horizontal swipe is handled in JS -->
</div>

// Registering a non-passive listener when preventDefault() must actually work
onMounted(() => {
  elementRef.value.addEventListener('touchmove', handleTouchMove, { passive: false })
})

function handleTouchMove(event) {
  if (isDragging.value) {
    event.preventDefault() // only works because passive: false was set above
  }
}

Diese beiden Mechanismen zusammen lösen die meisten Scroll-Konflikte bei Touch Gesten in Vue Apps: touch-action entscheidet grundsätzlich, welche Achse dem Browser gehört, während nicht-passive Listener in Ausnahmefällen erzwingen, dass eine JavaScript-Geste Vorrang vor dem nativen Scrollen bekommt.

7. Performance: 60fps unter Touch-Interaktion halten

Touch-Interaktionen sind besonders anfällig für sichtbares Ruckeln, weil Nutzer die Bewegung des eigenen Fingers direkt mit dem visuellen Feedback vergleichen, was Verzögerungen sofort auffällig macht. Für flüssige Touch Gesten in Vue Apps sollten Positions-Updates über CSS-Transformationen statt über top/left-Eigenschaften erfolgen, weil transform vom Browser auf der GPU beschleunigt gerendert wird, ohne Layout-Neuberechnung auszulösen.

Ein zweiter Performance-Hebel betrifft die Häufigkeit der reaktiven Updates. Bei sehr schnellen Pointer-Bewegungen können pointermove-Events häufiger feuern, als der Browser rendern kann. Das Bündeln von Positions-Updates in requestAnimationFrame stellt sicher, dass Vue-Reaktivität und DOM-Updates synchron zum Bildschirm-Refresh laufen, statt bei jedem einzelnen Event unnötig oft zu aktualisieren.


// Throttling position updates to the browser's paint cycle
let rafId = null

function handlePointerMove(event) {
  if (rafId) return // a frame is already scheduled, skip this event
  rafId = requestAnimationFrame(() => {
    offsetX.value = event.clientX - startX.value // GPU-accelerated via transform
    rafId = null
  })
}

Für komplexe Listen mit vielen gleichzeitig sichtbaren Swipe-Karten ist zusätzlich Virtualisierung sinnvoll, damit nur die tatsächlich sichtbaren Karten Pointer-Listener registrieren. Ohne diese Einschränkung akkumulieren sich bei langen Listen unnötig viele aktive Event-Listener, die auch dann Rechenzeit verbrauchen, wenn der Nutzer gerade eine ganz andere Karte berührt.

8. Zugänglichkeit: Tastatur-Alternativen für Gesten

Ein oft vernachlässigter Aspekt bei Touch Gesten in Vue Apps ist die Zugänglichkeit für Nutzer, die keine Touch-Eingabe verwenden, etwa bei Tastatur-Navigation oder assistiven Technologien. Jede Aktion, die per Swipe erreichbar ist, etwa das Löschen eines Listen-Elements, sollte zusätzlich über einen regulären, fokussierbaren Button erreichbar sein, der über die Tastatur bedienbar ist.

Für Drag-and-Drop-Reordering gilt dasselbe Prinzip: Eine rein touch- oder maus-basierte Drag-Implementierung schließt Tastaturnutzer komplett aus. Alternative Pfeiltasten-Steuerung oder dedizierte "Nach oben"/"Nach unten"-Buttons neben jedem Listen-Element stellen sicher, dass dieselbe Funktionalität ohne Zeigegerät nutzbar bleibt. Bibliotheken wie VueDraggable unterstützen solche Tastatur-Alternativen teilweise bereits, eine eigene Prüfung im konkreten Projekt bleibt aber notwendig.

9. VueUse vs. Hammer.js vs. eigene Implementierung

Für Touch Gesten in Vue Apps stehen grundsätzlich drei Wege offen, die sich in Umfang, Bundle-Größe und Kontrolle unterscheiden.

Ansatz Bundle-Größe Vue-Integration Kontrolle
VueUse Klein, Tree-Shaking-fähig Nativ, Composables Gut für Standard-Gesten
Hammer.js Mittel, kein Tree-Shaking Manuell anzubinden Sehr umfangreiche Gesten-Bibliothek
Eigene Pointer-Events-Logik Minimal, nur genutzter Code Vollständig Vue-nativ Maximal, aber mehr Wartungsaufwand

Für die meisten Projekte ist VueUse der pragmatischste Startpunkt, weil die Composables bereits Kantenfälle wie Passive-Listener, Pointer-Capture und Threshold-Konfiguration abdecken. Hammer.js lohnt sich vor allem, wenn sehr viele unterschiedliche, komplexe Gesten gleichzeitig benötigt werden und die zusätzliche Bundle-Größe akzeptabel ist. Eine eigene Implementierung auf Basis von Pointer Events bleibt sinnvoll für sehr spezifische Anforderungen, die von keiner Bibliothek exakt abgedeckt werden, etwa die Pinch-to-Zoom-Logik aus Abschnitt fünf.

Mironsoft

Vue-Entwicklung, Mobile UX und Touch-Interaktionsdesign

Touch-Interaktionen, die sich wirklich nativ anfühlen?

Wir implementieren Swipe-, Drag- und Pinch-Gesten in Vue-Apps, lösen Scroll-Konflikte sauber und sorgen für flüssige 60fps-Interaktionen auch auf älteren Mobilgeräten.

Gesten-Audit

Bestehende Touch-Interaktionen auf Scroll-Konflikte und Ruckeln prüfen

Composable-Bibliothek

Wiederverwendbare Swipe-, Drag- und Pinch-Composables aufbauen

Accessibility-Fallbacks

Tastatur-Alternativen für jede Gesten-basierte Aktion ergänzen

10. Zusammenfassung

Touch Gesten in Vue Apps brauchen mehr Sorgfalt als einfache Click-Handler, weil sie über die Zeit ablaufen, mit dem nativen Scroll-Verhalten konkurrieren und auf verschiedenen Geräten unterschiedlich präzise Eingaben liefern. Pointer Events vereinheitlichen Maus-, Touch- und Stift-Eingaben unter einem Event-Modell, während touch-action und nicht-passive Listener gezielt steuern, welche Achse dem Browser und welche der eigenen JavaScript-Logik gehört.

Swipe-, Drag- und Pinch-Erkennung profitieren von der Trennung zwischen kontinuierlicher visueller Rückmeldung und abschließender Aktionsentscheidung, während requestAnimationFrame-Bündelung und CSS-Transformationen für flüssige 60fps-Performance sorgen. VueUse deckt die meisten Standardfälle bereits ab, eigene Pointer-Events-Logik bleibt für Spezialfälle wie Pinch-to-Zoom die flexiblere Wahl. Tastatur-Alternativen für jede Gesten-Aktion runden eine zugängliche Implementierung ab.

Touch Gesten in Vue Apps — Das Wichtigste auf einen Blick

Pointer Events

Ein einheitliches Event-Modell für Maus, Touch und Stift, ergänzt um setPointerCapture.

Scroll-Konflikte

touch-action steuert, welche Achse nativ scrollt, nicht-passive Listener erzwingen preventDefault().

Performance

requestAnimationFrame und CSS-transform halten Touch-Interaktionen bei 60fps.

Zugänglichkeit

Jede Gesten-Aktion braucht eine fokussierbare Tastatur-Alternative.

11. FAQ: Touch Gesten in Vue Apps

1Was sind Pointer Events?
Ein einheitliches Event-Modell für Maus, Touch und Stift statt separater Handler pro Eingabetyp.
2Swipe vs. Scroll unterscheiden?
Über touch-action: pan-y plus Winkelprüfung der Bewegungsrichtung.
3Warum wirkt preventDefault() nicht?
Der Listener ist standardmäßig passiv, muss explizit mit { passive: false } registriert werden.
4Was macht setPointerCapture?
Bindet Folge-Events ans Zielelement, auch wenn der Finger während der Bewegung darüber hinauswandert.
5Swipe zuverlässig erkennen?
Kombination aus Mindestdistanz und Mindestgeschwindigkeit statt reiner Distanzprüfung.
660fps bei Touch halten?
CSS-transform statt top/left, pointermove-Events mit requestAnimationFrame bündeln.
7Pinch-to-Zoom implementieren?
Zwei Pointer per pointerId verfolgen, Abstand berechnen, Zoom-Stufe aus dem Verhältnis ableiten.
8Ohne Tastatur-Alternative barrierefrei?
Nein, jede Gesten-Aktion braucht einen fokussierbaren, tastaturbedienbaren Ersatz.
9VueUse oder eigene Lösung?
VueUse für Standardfälle, eigene Pointer-Events-Logik für sehr spezielle Anforderungen.
10Zu viele Listener bei langen Listen?
Virtualisierung nutzen, sodass nur sichtbare Elemente Pointer-Listener registrieren.