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.

Swipe – durch eine Galerie wischen

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.

EventBeschreibung
pointerdownDer Finger wird aufgesetzt oder die Maus klickt
pointermoveBenutzer bewegt den Finger, die Maus der den Pen ohne hochzuheben
pointerupZeigegerät oder Finger werden angehoben
pointercancelEingabe abgebrochen
pointercapture, lostpointercaptureEin 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:

InfoBeschreibung
pointerIdEindeutige ID für jeden Finger, jeden Stift (wichtig für Multitouch)
pointerTypemouse, pen, touch
pressureDruck von 0 bis 1 (Stifte)
tiltX, tiltYNeigung des Stifts
width, heightFlä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));
Suchen auf mediaevent.de