Javascript Pointer-Events: Swipe für Touch, Pen und Maus
Pointer-Events ersetzen Touch- und Maus-Events und erlauben eine einheitliche Gestensteuerung (Swipe, Pinch, Rotate). Sie realisieren Zeichen-Apps mit Druck und Stiftunterstützung, Drag & Drop auf Touch- und Desktop und bieten ein einheitliches Handling für Spiele.
Ein swipe- oder onswipe-Event gibt's nicht
Swipe oder Wischen ist eine Geste auf Touchscreens, bei der ein Finger oder der Stift auf dem Touchscreen oder die Maus auf dem Desktop-Monitor gezogen wird. Eine Swipe-Geste wischt oder zieht Bilder in einem Karussel oder Slider. Ein Swipe von Rechts nach Links oder Links nach Rechts holt den Screen einer zuvor oder danach besuchten Seite auf den Screen.
Ein Event für jede denkbare Geste (swipe, pinch, rotate, …) wäre nicht sinnvoll. Stattdessen gibt es primitive Events: touchstart, touchmove, touchend auf mobilen Geräten, und für die Aktionen mit der Maus anfangs mousedown, mousemove, mouseup. Plattformübergreifend sind pointerdown, pointermove, pointerup für Touchscreen, Stylus und Maus später aufgenommen worden, um die Doppelprogrammierung von Gesten zu vermeiden.
Event | Beschreibung |
---|---|
pointerdown | Der Finger wird aufgesetzt oder die Maus klickt |
pointermove | Benutzer bewegt den Finger, die Maus der den Pen ohne hochzuheben |
pointerup | Zeigegerät oder Finger werden angehoben |
pointercancel | Eingabe abgebrochen |
pointercapture, lostpointercapture | Ein Element hält den Pointer exklusiv fest |
Um ein einfaches Swipe von rechts nach links oder von links nach rechts zu entdecken, braucht Javascript nur wenige Zeilen. Hinter Swipe steckt ein einfaches pointerdown-Event sowie das Auslesen von pointerup, auf welchen Koordinaten Finger, Pen oder Maus die Fläche verlassen.
const area = document.querySelector(".swipeDetector"); let startX = 0; let startY = 0; let isSwiping = false; area.addEventListener("pointerdown", (e) => { startX = e.clientX; startY = e.clientY; isSwiping = true; }); area.addEventListener("pointerup", (e) => { if (!isSwiping) return; isSwiping = false; const dx = e.clientX - startX; const dy = e.clientY - startY; // Nur horizontale Bewegungen, und Schwelle setzen if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) { if (dx > 0) { document.querySelector(".swipeResult").textContent = "Swipe nach rechts"; area.style.backgroundColor = "#a0e3a0"; } else { document.querySelector(".swipeResult").textContent = "Swipe nach links"; area.style.backgroundColor = "#e3a0a0"; } } });
- pointerdown speichert die Startkoordinaten.
- Beim pointerup werden Start- mit Endkoordinaten verglichen.
- Wenn die horizontale Bewegung groß genug ist (> 50px) und stärker als die vertikale Bewegung, wird es als Swipe gewertet.
Am Rande: In CSS ist die Eigenschaft scroll-snap zuverlässig in den modernen Browsern angekommen. Ein Stupser mit dem Finger oder ein Wischen mit der Maus reicht.
Pointer-Events
Heute ist ein Swipe mit Vanilla Javascript mit Pointer Events (pointerdown, pointermove, pointerup) für Maus, Touchscreen und Stylus einfach umzusetzen. Das läuft auf Touch, Maus, Stylus ohne Zusatzbibliotheken. Die Altlasten wie jQuery und hammer.js werden nicht mehr gebraucht.
Pointer Events liefern mehr Informationen als die klassischen Mouse-/Touch-Events:
Info | Beschreibung |
---|---|
pointerId | Eindeutige ID für jeden Finger, jeden Stift (wichtig für Multitouch) |
pointerType | mouse, pen, touch |
pressure | Druck von 0 bis 1 (Stifte) |
tiltX, tiltY | Neigung des Stifts |
width, height | Fläche der Berührung (Fingergröße) |
const element = document.querySelector(".elem") element.addEventListener("pointerdown", (e) => { element.innerHTML = `Pointer gestartet
${e.pointerType} ID ${e.pointerId}`; }); element.addEventListener("pointermove", (e) => { if (e.pointerType === "touch") { element.innerHTML = `Finger bewegt
X ${e.clientX}
Y ${e.clientY}`; } }); element.addEventListener("pointerup", (e) => { element.textContent = `Pointer losgelassen`; });
Carousel mit Pointer Events – Swipe auf dem Touchscreen
- Pointer Events: läuft auf Touch, Maus, Stift – ein Codepfad für alle Zeigegeräte.
- Drag-Follow: der Slide »folgt« dem Finger/der Maus live.
- Nahtloses Looping: via erstem/letztem Klon + »unsichtbarem« Zurückspringen nach der Transition.
- Dots & Buttons: Klickbar, ARIA-Attribute für bessere Zugänglichkeit, Pfeiltasten-Support.
- Die Gallery ist responsiv und passt sich der Containerbreite an; Threshold skaliert mit.
- Scroll-freundlich: touch-action:pan-y lässt vertikales Scrollen der Seite zu.
- Die Slides wechseln automatisch im Intervall.
- Die Animation pausiert, wenn die Maus drüber hovert oder wenn das Karussell den Fokus bekommt (z. B. per Tastatur-Navigation) und startet wieder, wenn die Maus die Gallery verläßt / rausgeht.
<div class="carousel" aria-roledescription="karussell" aria-label="Bildergalerie"> <div class="track" id="track"> <div class="slide"><img decoding="async" src="landschaft-04.webp" width="1440" height="811" alt=""></div> <div class="slide"><img decoding="async" src="landschaft-03.webp" width="1440" height="811" alt=""></div> … </div> <div class="nav" aria-hidden="true"> <button class="btn" id="prev" title="Vorher">‹</button> <button class="btn" id="next" title="Nächste">›</button> </div> <div class="dots" id="dots" role="tablist" aria-label="Slides wählen"></div> </div>
:root { --w: 920px; --h: 400px; } * { box-sizing: border-box; } .carousel { width: min(100vw - 32px, var(--w)); height: var(--h); overflow: hidden; border-radius: 14px; box-shadow: 0 6px 18px rgba(0,0,0,.15); user-select: none; position: relative; background: #111; touch-action: pan-y; } .track { display: flex; height: 100%; transition: transform .35s ease; will-change: transform; } .slide { flex: 0 0 100%; height: 100%; position: relative; } .slide img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; -webkit-user-drag: none; } /* Dots */ .dots { position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); display: flex; gap: 8px; padding: 6px 10px; background: rgba(0,0,0,.25); border-radius: 20px; backdrop-filter: blur(6px); } .dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,.6); cursor: pointer; outline: none; border: 0; } .dot[aria-current="true"] { background: white; } /* Buttons */ .nav { position: absolute; inset: 0; display: flex; justify-content: space-between; align-items: center; pointer-events: none; } .btn { pointer-events: auto; width: 40px; height: 40px; border-radius: 50%; border: 0; background: rgba(0,0,0,.35); color: white; font-size: 18px; display: grid; place-items: center; cursor: pointer; }
const carousel = document.querySelector('.carousel'); const track = document.getElementById('track'); const dotsWrap = document.getElementById('dots'); const prevBtn = document.getElementById('prev'); const nextBtn = document.getElementById('next'); // Originale Slides let slides = Array.from(track.children); const N = slides.length; // Klone für Loop const firstClone = slides[0].cloneNode(true); const lastClone = slides[slides.length - 1].cloneNode(true); track.insertBefore(lastClone, track.firstChild); track.appendChild(firstClone); slides = Array.from(track.children); let index = 1; // Start function slideWidth() { return carousel.clientWidth; } function setPosition(instant = false) { if (instant) track.style.transition = 'none'; track.style.transform = `translateX(${-index * slideWidth()}px)`; if (instant) requestAnimationFrame(() => { track.style.transition = 'transform .35s ease'; }); updateDots(); } // Dots const dots = []; for (let i = 0; i < N; i++) { const b = document.createElement('button'); b.className = 'dot'; b.setAttribute('role', 'tab'); b.setAttribute('aria-label', `Folie ${i+1}`); b.addEventListener('click', () => goToReal(i)); dotsWrap.appendChild(b); dots.push(b); } function realIndex() { if (index === 0) return N - 1; if (index === N + 1) return 0; return index - 1; } function updateDots() { const r = realIndex(); dots.forEach((d, i) => d.setAttribute('aria-current', i === r ? 'true' : 'false')); } function goToReal(i, instant = false) { index = i + 1; setPosition(instant); } function next() { index++; snap(); } function prev() { index--; snap(); } function snap() { setPosition(); } // Damit beim "Rücksprung nicht immer ein kurzes Aufflackern aller Slides zu sehen ist, // wird eine doppelte request AnimationFrame-Variante benutzt // Das „Aufflackern“ beim Rücksprung ist ein bekanntes Problem bei Loop-Carousels mit geklonten Slides. track.addEventListener('transitionend', () => { if (index === 0 || index === N + 1) { // Zielindex festlegen index = (index === 0) ? N : 1; // Im nächsten Frame die Transition ausschalten + neue Position setzen requestAnimationFrame(() => { track.style.transition = 'none'; setPosition(); // Noch einen Frame warten, dann Transition zurücksetzen requestAnimationFrame(() => { track.style.transition = 'transform .35s ease'; }); }); } }); // Drag/Swipe let dragging = false; let startX = 0; let lastX = 0; let pointerId = null; const THRESHOLD = () => Math.max(50, slideWidth() * 0.15); function onDown(e) { pointerId = e.pointerId; track.setPointerCapture(pointerId); dragging = true; startX = lastX = e.clientX; track.style.transition = 'none'; stopAutoplay(); // beim Drag pausieren } function onMove(e) { if (!dragging) return; lastX = e.clientX; const dx = lastX - startX; const base = -index * slideWidth(); track.style.transform = `translateX(${base + dx}px)`; } function onUpOrCancel() { if (!dragging) return; dragging = false; try { track.releasePointerCapture(pointerId); } catch {} const dx = lastX - startX; track.style.transition = 'transform .35s ease'; if (Math.abs(dx) >= THRESHOLD()) { if (dx < 0) index++; else index--; } snap(); startAutoplay(); // Autoplay wieder starten } track.addEventListener('pointerdown', onDown); track.addEventListener('pointermove', onMove); track.addEventListener('pointerup', onUpOrCancel); track.addEventListener('pointercancel', onUpOrCancel); // Buttons prevBtn.addEventListener('click', prev); nextBtn.addEventListener('click', next); // Keyboard carousel.tabIndex = 0; carousel.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') prev(); if (e.key === 'ArrowRight') next(); }); // Responsive new ResizeObserver(() => setPosition(true)).observe(carousel); // Autoplay let timer = null; const DELAY = 4000; // 4s function startAutoplay() { stopAutoplay(); timer = setInterval(() => next(), DELAY); } function stopAutoplay() { if (timer) clearInterval(timer); timer = null; } // Pausieren bei Hover oder Fokus carousel.addEventListener('mouseenter', stopAutoplay); carousel.addEventListener('mouseleave', startAutoplay); carousel.addEventListener('focusin', stopAutoplay); carousel.addEventListener('focusout', startAutoplay); // Init setPosition(true); startAutoplay(); // Extra: Lazy Loading verbessern – lädt das nächste/n vorherige Bild vor const observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute("data-src"); } observer.unobserve(img); } } }, { root: carousel, threshold: 0.1 }); document.querySelectorAll('.slide img').forEach(img => observer.observe(img));