JavaScript Maus-Koordinaten Abrufen

Jedes Tutorial erzaehlt dir, dass clientX existiert. Fast keines zeigt dir, warum es eine andere Zahl als offsetX liefert -- mit dem Beweis direkt vor dir.

Live-Demo: Vier Koordinatensysteme Gleichzeitig

Bewege die Maus ueber den karierten Bereich unten. Alle vier Koordinatensysteme aktualisieren sich gleichzeitig. Das ist der schnellste Weg, den Unterschied zu verstehen -- du kannst buchstaeblich sehen, wie screenX groesser als clientX bleibt, weil es von der physischen Monitor-Ecke zaehlt, nicht vom Browser-Viewport.

Bewege die Maus ueber diesen Bereich

0
0
0
0
0
0
0
0
Versuche, die Seite nach unten zu scrollen und dann die Maus zu bewegen -- beobachte, wie pageY waechst, aber clientY gleich bleibt.

Die Grundlagen: mousemove in einer Minute

Jede Maus-Koordinate in JavaScript beginnt mit dem mousemove-Event. Du haengst einen Listener an, liest das Event-Objekt und erhaeltst X- und Y-Werte. Das ist das gesamte Fundament:

document.addEventListener('mousemove', function(e) {
  console.log(e.clientX, e.clientY);
});

Das Event-Objekt e traegt einen Stapel von Koordinaten-Eigenschaften. Die vier, die du tatsaechlich brauchst, sind offsetX, clientX, pageX und screenX (plus ihre Y-Gegenstuecke). Sie alle beschreiben dieselbe physische Mausposition, aber gemessen von verschiedenen Referenzpunkten.

Der Fehler, den die meisten Tutorials machen: Sie zeigen dir clientX und fertig. Aber wenn du ein echtes Drag-and-Drop, ein Canvas-Zeichentool oder ein Tooltip-Positionierungssystem baust, verursacht die falsche Wahl der Koordinaten-Eigenschaft subtile, frustrierende Bugs. Die Demo oben existiert, damit du nie wieder raten musst.

offsetX vs clientX vs pageX vs screenX: Welches?

Hier ist die Vergleichstabelle, die auf jeder Tutorial-Seite haette sein sollen, aber nicht war. Jede Eigenschaft misst dieselbe Cursor-Position von einem anderen Ursprungspunkt:

EigenschaftReferenzpunktAm Besten FuerAendert sich beim Scrollen?
offsetX / offsetYDie Padding-Kante des Ziel-Elements (das Element unter dem Cursor)Hover-Effekte, element-lokale InteraktionenNein
clientX / clientYDie obere linke Ecke des Browser-ViewportsPositionieren von Tooltips, Dropdowns, Modals relativ zur sichtbaren SeiteNein
pageX / pageYDie obere linke Ecke des gesamten Dokuments (inklusive gescrolltem Bereich)Zeichnen auf Canvas, Verankern von Elementen beim ScrollenJa
screenX / screenYDie obere linke Ecke deines physischen Bildschirms (Monitor)Multi-Window-Apps, Browser-Erweiterungen, selten in normalem WebNein

Was Leute verwirrt: pageX inkludiert den Scroll-Offset, clientX nicht. Wenn deine Seite 3000px hoch ist und du 1000px nach unten scrollst, dann wird pageY etwa 1000 mehr als clientY an derselben Cursor-Position sein. Die Live-Demo oben beweist das -- scrolle und beobachte den Unterschied.

Faustregel: Nutze clientX/clientY fuer 90% der UI-Positionierungsaufgaben. Greife zu offsetX, wenn du dich nur fuer das Element unter dem Cursor interessierst. Nutze pageX, wenn du dokument-verankerte Koordinaten brauchst (Canvas-Zeichnung, absolut positionierte Elemente). Nutze fast nie screenX in normaler Web-Entwicklung.

getBoundingClientRect(): Das Schweizer Taschenmesser, das du Brauchst

Hier ist etwas, das Tutorials kaum behandeln: Die nuetzlichste Koordinaten-Funktion in JavaScript ist ueberhaupt keine Maus-Event-Eigenschaft. Es ist Element.getBoundingClientRect(). Sie gibt Groesse und Position eines Elements relativ zum Viewport zurueck:

var rect = element.getBoundingClientRect();
// rect.left, rect.top     -- distance from viewport top-left
// rect.right, rect.bottom -- opposite edges
// rect.width, rect.height -- element dimensions
// All values include CSS border but NOT margin

Die wahre Staerke: Subtrahiere rect.left von e.clientX, um die Maus-Position relativ zu diesem Element zu erhalten, unabhaengig davon, wo es auf der Seite sitzt. So baust du zuverlaessige Klick-Ziele, Canvas-Zeichnung und Drag-Zonen:

element.addEventListener('mousemove', function(e) {
  var rect = this.getBoundingClientRect();
  var localX = e.clientX - rect.left;  // X relative to this element
  var localY = e.clientY - rect.top;   // Y relative to this element
  console.log('Mouse inside element:', localX, localY);
});

Dies ist zuverlaessiger als offsetX in Edge-Cases. Wenn dein Element Kindelemente hat, kann offsetX ploetzlich springen, weil die Referenz zum Kind wechselt. Der getBoundingClientRect()-Ansatz gibt dir immer Koordinaten relativ zu dem Element, das dich wirklich interessiert.

CSS-Transforms: Wo Koordinaten Leise Brechen

Dieser Abschnitt stammt aus echten Bug-Reports. Wenn du transform: scale() oder transform: rotate() auf ein Element anwendest, luegen die Koordinaten-Eigenschaften -- und die Dokumentation erwaehnt es kaum.

transform: scale() verzerrt offsetX

Wenn du ein Element auf 50% skalierst, verkleinert der Browser die visuelle Groesse, aber offsetX meldet Koordinaten im skalierten Koordinatenraum. Wenn dein Element 200px breit und auf 0.5 skaliert ist, gibt das Bewegen der Maus zur visuellen Mitte offsetX = 50, nicht 100. getBoundingClientRect() liefert ebenfalls skalierte Dimensionen.

// WRONG: offsetX under scale() gives scaled coordinates
// If element is scaled to 0.5, offsetX is halved
element.style.transform = 'scale(0.5)';
element.addEventListener('mousemove', function(e) {
  console.log(e.offsetX); // Reports in scaled space
});

// CORRECT: Use getBoundingClientRect for true visual position
element.addEventListener('mousemove', function(e) {
  var rect = element.getBoundingClientRect();
  var visualX = e.clientX - rect.left;
  var visualY = e.clientY - rect.top;
  // To get the unscaled coordinate:
  var scaleX = element.offsetWidth / rect.width;
  var realX = visualX * scaleX;
  console.log('Unscaled X:', realX);
});

SVG-Elemente: offsetX verhaelt sich anders

In SVG koennen offsetX/offsetY sich auf die SVG-Wurzel oder die spezifische Kind-Form beziehen, je nach Browser. Der zuverlaessige Ansatz ist getBoundingClientRect() oder createSVGPoint():

// SVG: use createSVGPoint for accurate coordinates
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
svg.addEventListener('pointermove', function(e) {
  pt.x = e.clientX;
  pt.y = e.clientY;
  // Convert to SVG user space:
  var svgPoint = pt.matrixTransform(
    svg.getScreenCTM().inverse()
  );
  console.log('SVG coords:', svgPoint.x, svgPoint.y);
});

Pointer Events: Hoer auf, nur mousemove zu Nutzen

Wenn du immer noch separate Handler fuer mousemove, touchmove und touchstart schreibst, leistest du dreifache Arbeit fuer schlechtere Ergebnisse. Pointer Events vereinen Maus, Touch und Stift in einer einzigen API:

// Old way: three event types, three handlers
element.addEventListener('mousemove', handleMouse);
element.addEventListener('touchmove', handleTouch);  // different event shape!
element.addEventListener('touchstart', handleTouchStart);

// Modern way: one event type
element.addEventListener('pointermove', function(e) {
  // Same properties as MouseEvent:
  console.log(e.clientX, e.clientY, e.pointerType);
  // pointerType: 'mouse', 'touch', or 'pen'
});

Der entscheidende Vorteil: pointermove feuert fuer Maus, Finger und Stift mit der exakt gleichen clientX/clientY-Schnittstelle. Kein Mappen von touches[0] auf ein falsches Maus-Event mehr.

EventMausTouchStiftNotizen
mousemoveJaNein (emuliert)NeinLegacy, Touch emuliert mousemove mit Verzoegerung
touchmoveNeinJaNeinNutzt touches-Array, andere API-Struktur
pointermoveJaJaJaVereinheitlichte API, gleiche Koordinaten-Schnittstelle

Browser-Unterstützung fuer Pointer Events ist mittlerweile universell (97%+ global). Solange du keine sehr alten Browser unterstuetzt, ist pointermove die richtige Wahl. Fuege touch-action: none CSS hinzu, um zu verhindern, dass der Browser Touch-Gesten fuer Scrollen uebernimmt.

Performance: Throttle vs requestAnimationFrame

Ein mousemove-Event kann auf einem schnellen Computer ueber 100-mal pro Sekunde feuern. Wenn dein Handler DOM-Updates, Layout-Berechnungen oder Canvas-Redraws macht, killst du die Performance. Zwei Loesungen: Throttling und requestAnimationFrame.

Throttling (Aufrufe pro Sekunde begrenzen)

// Throttle: max one call per 16ms (~60fps)
var lastTime = 0;
element.addEventListener('mousemove', function(e) {
  var now = Date.now();
  if (now - lastTime < 16) return;
  lastTime = now;
  doExpensiveWork(e.clientX, e.clientY);
});

requestAnimationFrame (mit Browser-Repaint synchronisieren)

// rAF: batch updates to the next frame
var pendingEvent = null;
element.addEventListener('mousemove', function(e) {
  pendingEvent = e;  // Store latest, drop intermediates
});
function update() {
  if (pendingEvent) {
    doExpensiveWork(pendingEvent.clientX, pendingEvent.clientY);
    pendingEvent = null;
  }
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

Was ist besser? requestAnimationFrame ist der ueberlegene Ansatz fuer visuelle Updates, weil es mit der Bildwiederholfrequenz synchronisiert und automatisch pausiert, wenn der Tab nicht sichtbar ist. Throttling ist einfacher fuer nicht-visuelle Aufgaben wie Analytics-Logging. Fuer alles, was Pixel auf dem Bildschirm bewegt, nutze rAF.

Gemessene Auswirkung

Auf einer typischen Seite mit Canvas-Redraw im Handler: rohes mousemove faellt unter starker Mausbewegung auf 12-15fps. Auf 60fps gedrosselt: fluessig, aber macht noch redundante Arbeit zwischen Frames. Mit rAF: bleibt bei 60fps ohne verschwendete Renders. Der Unterschied ist in echten Anwendungen sichtbar.

Produktionscode: Drag-and-Drop mit Begrenzung

Hier ist die Drag-and-Drop-Implementierung, die wir uns beim Start gewuenscht haetten. Sie nutzt pointermove (Maus/Touch vereint), schraenkt das Element in einem Container ein und raeumt die Listener ordentlich auf:

function makeDraggable(element, container) {
  var isDragging = false;
  var startX, startY, origLeft, origTop;

  element.addEventListener('pointerdown', function(e) {
    isDragging = true;
    startX = e.clientX;
    startY = e.clientY;
    var rect = element.getBoundingClientRect();
    origLeft = rect.left;
    origTop = rect.top;
    element.setPointerCapture(e.pointerId);
    e.preventDefault();
  });

  element.addEventListener('pointermove', function(e) {
    if (!isDragging) return;
    var deltaX = e.clientX - startX;
    var deltaY = e.clientY - startY;
    var newLeft = origLeft + deltaX;
    var newTop = origTop + deltaY;

    // Boundary clamping: keep element inside container
    var cRect = container.getBoundingClientRect();
    var eRect = element.getBoundingClientRect();
    var maxLeft = cRect.right - eRect.width;
    var maxTop = cRect.bottom - eRect.height;
    newLeft = Math.max(cRect.left, Math.min(newLeft, maxLeft));
    newTop = Math.max(cRect.top, Math.min(newTop, maxTop));

    element.style.left = newLeft + 'px';
    element.style.top = newTop + 'px';
  });

  element.addEventListener('pointerup', function(e) {
    isDragging = false;
    element.releasePointerCapture(e.pointerId);
  });
}

Zwei Dinge machen sie produktionsreif: setPointerCapture() stellt sicher, dass das Element weiterhin Events erhaelt, auch wenn der Cursor waehrend des Ziehens das Element verlaesst, und die Begrenzungs-Klemmung nutzt getBoundingClientRect(), um tatsaechlich gerenderte Groessen zu respektieren statt aus CSS-Werten zu raten.

Canvas-Koordinaten-Mapping

Zeichnen auf einem Canvas erfordert das Mappen von Maus-Koordinaten in den Canvas-Pixel-Raum. Wenn das Canvas eine andere CSS-Groesse als seine interne Aufloesung hat (was bei HiDPI-Displays der Fall sein sollte), bricht der naive Ansatz:

var canvas = document.getElementById('my-canvas');
var ctx = canvas.getContext('2d');

// Set internal resolution for HiDPI
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);

canvas.addEventListener('pointermove', function(e) {
  var rect = canvas.getBoundingClientRect();
  // CSS pixel coordinates (what you draw with):
  var cssX = e.clientX - rect.left;
  var cssY = e.clientY - rect.top;
  // If you need raw canvas pixels:
  var pixelX = cssX * dpr;
  var pixelY = cssY * dpr;
  ctx.fillRect(cssX - 2, cssY - 2, 4, 4); // Draw at cursor
});

Die dpr-Skalierung (Device Pixel Ratio) ist auf Retina/HiDPI-Displays unerlaesslich. Ohne sie erscheint dein Canvas-Zeichnung unscharf und Koordinaten-Klicks sind leicht verschoben. Wir behandeln das ausfuehrlich in unserem Leitfaden zu physischen vs logischen Pixeln.

FAQ: Haeufige Probleme

Warum springt offsetX, wenn die Maus ein Kindelement ueberquert?

offsetX ist relativ zu dem Element, das sich direkt unter dem Cursor befindet. Wenn du dich von einem Eltern- zu einem Kindelement bewegst, springt der Referenzpunkt zur Ecke des Kindes. Nutze e.clientX - element.getBoundingClientRect().left fuer stabile element-relative Koordinaten.

Funktionieren Maus-Koordinaten in einem iframe?

Ja, aber screenX/screenY melden Werte relativ zur Position des iframes innerhalb der Eltern-Seite, nicht des echten Monitors. clientX und pageX funktionieren normal innerhalb des eigenen iframe-Dokuments. Koordinaten-Uebersetzung zwischen Frames erfordert postMessage-Kommunikation.

Was passiert mit Koordinaten in Shadow DOM?

Maus-Events werden beim Ueberqueren von Shadow-DOM-Grenzen neu ausgerichtet. e.target kann auf den Shadow Host statt auf das interne Element zeigen. event.composedPath() gibt dir den echten Event-Pfad durch Shadow-Grenzen. offsetX/offsetY referenzieren den Shadow Host, nicht das interne Element.

Werden Koordinaten von CSS-Transforms auf Eltern-Elementen beeinflusst?

Ja. getBoundingClientRect() gibt die visuell transformierte Position zurueck. Wenn also ein Eltern-Element transform: rotate(45deg) hat, spiegelt das Rect das rotierte Layout wider. clientX/clientY sind immer im Viewport-Raum und werden nicht von Transforms beeinflusst, aber die Position des Elements relativ zu diesen Koordinaten schon.

Was ist movementX/movementY?

Diese Eigenschaften melden das Delta (die Aenderung) seit dem letzten mousemove-Event, keine absolute Position. Sie sind nuetzlich fuer Dinge wie Pointer Lock (FPS-artige Kamera-Steuerung), wo es darauf ankommt, wie weit sich die Maus bewegt hat, nicht wo sie ist.

Sollte ich pageX oder clientX verwenden?

Nutze clientX fuer die meisten UI-Positionierungen (Tooltips, Modals, Popovers), weil es den Scroll-Offset ignoriert. Nutze pageX, wenn du eine Position ueber Scroll-Aenderungen hinweg merken musst (z.B. einen Punkt in einem langen Dokument markieren). Der Unterschied ist nur der Scroll-Offset: pageX = clientX + window.scrollX.

Weitermachen: Jetzt, wo du die JavaScript-Seite verstehst, sieh dir diese Koordinaten in verschiedenen Kontexten in Aktion an:

Maus-Position-Detektor — Live-Tool, das alle vier Koordinatensysteme mit Klick-zum-Erfassen zeigt

Pixel-Koordinaten Finden — Plattform-Methoden fuer Windows, macOS, Linux und Browser

Physische vs Logische Pixel — Warum sich deine Koordinaten zwischen CSS- und Geraete-Pixeln unterscheiden

Bildschirm-Koordinaten-Leitfaden — Vollstaendige Referenz fuer Bildschirm-Koordinaten-Konzepte

Startseite