JavaScript Obtener Coordenadas del Mouse
Cada tutorial te dice que clientX existe. Casi ninguno te muestra por que da un numero diferente a offsetX -- con la prueba justo frente a ti.
Demo en Vivo: Cuatro Sistemas de Coordenadas a la Vez
Mueve el mouse sobre el area cuadriculada de abajo. Los cuatro sistemas de coordenadas se actualizan simultaneamente. Esta es la forma mas rapida de entender la diferencia entre ellos -- puedes ver literalmente como screenX se mantiene mayor que clientX porque cuenta desde la esquina de tu monitor fisico, no del viewport del navegador.
Mueve el mouse sobre esta area
pageY crece pero clientY se mantiene igual.Lo Basico: mousemove en Un Minuto
Cada coordenada del mouse en JavaScript comienza con el evento mousemove. Adjuntas un listener, lees el objeto del evento y obtienes valores X e Y. Esa es toda la base:
document.addEventListener('mousemove', function(e) {
console.log(e.clientX, e.clientY);
});
El objeto del evento e lleva una pila de propiedades de coordenadas. Las cuatro que realmente usaras son offsetX, clientX, pageX y screenX (mas sus contrapartes Y). Todas describen la misma posicion fisica del mouse, pero medidas desde diferentes puntos de referencia.
El error que cometen la mayoria de tutoriales: te muestran clientX y dan por terminado el tema. Pero cuando construyes un arrastrar-y-soltar real, una herramienta de dibujo en canvas o un sistema de posicionamiento de tooltips, elegir la propiedad de coordenadas equivocada causa bugs sutiles y frustrantes. La demo de arriba existe para que nunca tengas que adivinar.
offsetX vs clientX vs pageX vs screenX: Cual?
Aqui esta la tabla de comparacion que deberia haber estado en cada pagina de tutorial pero no estaba. Cada propiedad mide la misma posicion del cursor desde un punto de origen diferente:
| Propiedad | Punto de Referencia | Mejor Para | Cambia con Scroll? |
|---|---|---|---|
offsetX / offsetY | El borde de padding del elemento destino (el elemento bajo el cursor) | Efectos hover, interacciones locales del elemento | No |
clientX / clientY | La esquina superior izquierda del viewport del navegador | Posicionar tooltips, menus desplegables, modales relativo a la pagina visible | No |
pageX / pageY | La esquina superior izquierda del documento completo (incluyendo area scrolleada) | Dibujar en canvas, anclar elementos que deben quedarse al hacer scroll | Si |
screenX / screenY | La esquina superior izquierda de tu pantalla fisica (el monitor) | Aplicaciones multi-ventana, extensiones del navegador, raramente necesario en web normal | No |
Lo que confunde a la gente: pageX incluye el offset de scroll, clientX no. Si tu pagina tiene 3000px de alto y haces scroll 1000px hacia abajo, entonces pageY sera aproximadamente 1000 mas que clientY en la misma posicion del cursor. La demo en vivo de arriba lo demuestra -- haz scroll y observa la diferencia.
Regla general: usa clientX/clientY para el 90% de las tareas de posicionamiento de UI. Usa offsetX cuando solo te importa el elemento bajo el cursor. Usa pageX cuando necesitas coordenadas ancladas al documento (dibujo en canvas, elementos con posicion absoluta). Casi nunca uses screenX en desarrollo web normal.
getBoundingClientRect(): La Navaja Suiza que Necesitas
Aqui hay algo que los tutoriales apenas cubren: la funcion de coordenadas mas util en JavaScript no es una propiedad de evento del mouse en absoluto. Es Element.getBoundingClientRect(). Devuelve el tamano y posicion de cualquier elemento relativo al viewport:
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
El verdadero poder: resta rect.left de e.clientX para obtener la posicion del mouse relativa a ese elemento, sin importar donde este en la pagina. Asi es como construyes objetivos de clic confiables, dibujo en canvas y zonas de arrastre:
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);
});
Esto es mas confiable que offsetX en casos extremos. Cuando tu elemento tiene elementos hijos, offsetX puede saltar de repente porque la referencia cambia al hijo. El enfoque de getBoundingClientRect() siempre te da coordenadas relativas al elemento que realmente te importa.
Transforms CSS: Donde las Coordenadas se Rompen Silenciosamente
Esta es la seccion nacida de reportes de bugs reales. Si aplicas transform: scale() o transform: rotate() a un elemento, las propiedades de coordenadas empiezan a mentirte -- y la documentacion apenas lo menciona.
transform: scale() distorsiona offsetX
Cuando escalas un elemento al 50%, el navegador reduce el tamano visual pero offsetX reporta coordenadas en el espacio de coordenadas escalado. Si tu elemento tiene 200px de ancho y esta escalado a 0.5, mover el mouse al centro visual da offsetX = 50, no 100. getBoundingClientRect() tambien devuelve dimensiones escaladas.
// 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);
});
Elementos SVG: offsetX se comporta diferente
En SVG, offsetX/offsetY pueden referenciar la raiz SVG o la forma hija especifica dependiendo del navegador. El enfoque confiable es getBoundingClientRect() o 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: Deja de Usar Solo mousemove
Si sigues escribiendo manejadores separados para mousemove, touchmove y touchstart, estas haciendo tres veces el trabajo por peores resultados. Pointer Events unifican mouse, tactil y lapiz en una sola 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'
});
La ventaja clave: pointermove se dispara para mouse, dedo y lapiz con la misma interfaz clientX/clientY. Adios mapear touches[0] a un evento de mouse falso.
| Evento | Mouse | Tactil | Lapiz | Notas |
|---|---|---|---|---|
mousemove | Si | No (emulado) | No | Legacy, el tactil emula mousemove con retraso |
touchmove | No | Si | No | Usa array touches, API con estructura diferente |
pointermove | Si | Si | Si | API unificada, misma interfaz de coordenadas |
El soporte de Pointer Events en navegadores ya es universal (97%+ global). A menos que soportes navegadores muy antiguos, pointermove es la opcion correcta. Anade touch-action: none CSS para evitar que el navegador secuestre los gestos tactiles para scroll.
Rendimiento: Throttle vs requestAnimationFrame
Un evento mousemove puede dispararse mas de 100 veces por segundo en una computadora rapida. Si tu manejador hace actualizaciones de DOM, calculos de layout o redibujos de canvas, arruinaras el rendimiento. Dos soluciones: throttling y requestAnimationFrame.
Throttling (limitar llamadas por segundo)
// 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 (sincronizar con el repaint del navegador)
// 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);
Cual es mejor? requestAnimationFrame es el enfoque superior para actualizaciones visuales porque se sincroniza con la tasa de refresco de la pantalla y se pausa automaticamente cuando la pestana no es visible. El throttling es mas simple para tareas no visuales como logging de analitica. Para cualquier cosa que mueva pixeles en pantalla, usa rAF.
Impacto medido
En una pagina tipica con redibujo de canvas en el manejador: mousemove puro cae a 12-15fps bajo movimiento intenso del mouse. Con throttle a 60fps: fluido pero aun hace trabajo redundante entre frames. Con rAF: se mantiene a 60fps con cero renders desperdiciados. La diferencia es visible en aplicaciones reales.
Codigo de Produccion: Arrastrar-y-Soltar con Limites
Aqui esta la implementacion de arrastrar-y-soltar que hubieramos querido al empezar. Usa pointermove (mouse/tactil unificado), limita el elemento dentro de un contenedor y limpia los listeners correctamente:
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);
});
}
Dos cosas la hacen lista para produccion: setPointerCapture() asegura que el elemento siga recibiendo eventos incluso si el cursor sale de el a mitad del arrastre, y la limitacion de limites usa getBoundingClientRect() para respetar los tamanos renderados reales en lugar de adivinar con valores CSS.
Mapeo de Coordenadas en Canvas
Dibujar en un canvas requiere mapear las coordenadas del mouse al espacio de pixeles del canvas. Cuando el canvas tiene un tamano CSS diferente de su resolucion interna (lo cual deberia para pantallas HiDPI), el enfoque ingenuo se rompe:
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
});
El escalado dpr (device pixel ratio) es esencial en pantallas Retina/HiDPI. Sin el, tu dibujo en canvas aparece borroso y los clics coordenados estan ligeramente desfasados. Lo cubrimos en profundidad en nuestra guia de pixeles fisicos vs logicos.
FAQ: Problemas Comunes
Por que offsetX salta cuando el mouse cruza un elemento hijo?
offsetX es relativo a cualquier elemento que este directamente bajo el cursor. Cuando te mueves de un padre a un hijo, el punto de referencia salta a la esquina del hijo. Usa e.clientX - element.getBoundingClientRect().left para coordenadas estables relativas al elemento.
Las coordenadas del mouse funcionan dentro de un iframe?
Si, pero screenX/screenY reportan valores relativos a la posicion del iframe dentro de la pagina padre, no del monitor real. clientX y pageX funcionan normalmente dentro del propio documento del iframe. La traduccion de coordenadas entre frames requiere comunicacion postMessage.
Que pasa con las coordenadas en Shadow DOM?
Los eventos de mouse se redirigen al cruzar los limites de Shadow DOM. e.target puede apuntar al shadow host en lugar del elemento interno. event.composedPath() te da la ruta real del evento a traves de los limites shadow. offsetX/offsetY referencian el shadow host, no el elemento interno.
Las coordenadas se ven afectadas por transforms CSS en elementos padres?
Si. getBoundingClientRect() devuelve la posicion visualmente transformada, asi que si un padre tiene transform: rotate(45deg), el rect reflejara el layout rotado. clientX/clientY siempre estan en el espacio del viewport y no se ven afectados por transforms, pero la posicion del elemento relativa a esas coordenadas si.
Que es movementX/movementY?
Estas propiedades reportan el delta (cambio) desde el ultimo evento mousemove, no una posicion absoluta. Son utiles para implementar cosas como pointer lock (controles de camara estilo FPS) donde te importa cuanto se movio el mouse, no donde esta.
Debo usar pageX o clientX?
Usa clientX para la mayoria del posicionamiento de UI (tooltips, modales, popovers) porque ignora el offset de scroll. Usa pageX cuando necesitas recordar una posicion a traves de cambios de scroll (como marcar un punto en un documento largo). La diferencia es solo el offset de scroll: pageX = clientX + window.scrollX.
Continua: Ahora que entiendes el lado de JavaScript, mira estas coordenadas en accion en diferentes contextos:
Detector de Posicion del Mouse — Herramienta en vivo mostrando los cuatro sistemas de coordenadas con captura por clic
Como Encontrar Coordenadas de Pixel — Metodos por plataforma para Windows, macOS, Linux y navegador
Pixeles Fisicos vs Logicos — Por que tus coordenadas difieren entre pixeles CSS y de dispositivo
Guia de Coordenadas de Pantalla — Referencia completa de conceptos de coordenadas de pantalla