JavaScript Coordinates
JavaScript is a language used to make websites interactive. It can do things like move objects on a screen or respond to what a user does. Understanding
Coordinates are numeric values that describe a position on the page. Almost every interactive feature — dragging, drawing, tooltips, pop-up menus, hit detection — comes down to reading a coordinate and placing or comparing something against it.
The catch is that the browser exposes two different coordinate systems, and mixing them up is the most common source of "my element jumps to the wrong place" bugs. This chapter explains both systems, how to convert between them, and the key DOM methods (getBoundingClientRect() and elementFromPoint()) you use to read element positions.
The two coordinate systems
There are two reference frames you need to keep straight:
- Viewport (window-relative) coordinates — measured from the top-left corner of the visible part of the page. These do not change when you scroll. A point at the top-left of what you can see is always
(0, 0). Properties:clientX/clientY. - Document (page-relative) coordinates — measured from the top-left of the whole document, including the part scrolled out of view. These increase as you scroll down. Properties:
pageX/pageY.
The relationship between them is just the scroll offset:
pageX = clientX + window.scrollX;
pageY = clientY + window.scrollY;When the page is not scrolled, window.scrollX and window.scrollY are both 0, so the two systems give identical numbers — which is exactly why scrolling bugs hide until someone scrolls the page.
Use viewport (clientX/Y) when… | Use document (pageX/Y) when… |
|---|---|
Positioning a position: fixed element | Positioning a position: absolute element |
| Hit-testing what is currently on screen | Storing a click location to recreate later |
Working with getBoundingClientRect() | Drawing on a tall scrollable canvas |
See JavaScript Window Sizes and Scrolling for how window.scrollX/Y and the viewport size are measured, and JavaScript Scrolling for moving the scroll position programmatically.
Reading coordinates from a mouse event
Every mouse event carries both coordinate systems. For the full event model, see Mouse Events Basics.
Viewport coordinates: clientX / clientY
event.clientX and event.clientY give the pointer position relative to the viewport, independent of scroll. The example below sits inside a tall page so you can scroll and confirm the numbers stay anchored to the visible area:
<!-- snippet: html-result -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Client-Side Coordinates Example</title>
<style>
#container {
width: 100%;
height: 100%;
background-color: grey;
min-height: 40px;
}
</style>
</head>
<body style="height: 2000px;">
<div id="container">Click anywhere in the grey area to see Client-Side coordinates!</div>
<script>
const container = document.getElementById('container');
function showCoords(event) {
alert("Client-Side X: " + event.clientX + ", Y: " + event.clientY);
}
container.addEventListener('click', showCoords);
</script>
</body>
</html>Document coordinates: pageX / pageY
event.pageX and event.pageY give the pointer position relative to the whole document, so they already include the scroll offset. They are standard and widely supported — there is no need to compute them by hand. The example below shows them next to the manual clientX + window.scrollX calculation so you can confirm the two match:
<!-- snippet: html-result -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Page-Side Coordinates Example</title>
<style>
#container {
width: 100%;
height: 100%;
background-color: grey;
min-height: 40px;
}
</style>
</head>
<body style="height: 2000px;">
<div id="container">Click anywhere in this area to see Page-Side coordinates!</div>
<script>
const container = document.getElementById('container');
function showPageCoords(event) {
// pageX/pageY already include scroll; the manual version must match
const manualX = event.clientX + window.scrollX;
const manualY = event.clientY + window.scrollY;
alert("pageX/Y: " + event.pageX + ", " + event.pageY +
"\nclientX+scrollX: " + manualX + ", " + manualY);
}
container.addEventListener('click', showPageCoords);
</script>
</body>
</html>Screen coordinates: screenX / screenY
event.screenX and event.screenY measure the pointer relative to the entire physical screen, not just the browser window. They are useful for tracking the cursor across multiple monitors or positioning a window.open() popup, but for in-page work you almost always want clientX/Y or pageX/Y instead.
Reading an element's position: getBoundingClientRect()
To find where an element sits (rather than where the mouse is), call getBoundingClientRect(). It returns a DOMRect whose top, right, bottom, left, width, and height are all in viewport coordinates — the same frame as clientX/Y.
const rect = element.getBoundingClientRect();
// rect.left, rect.top → top-left corner, relative to the viewport
// rect.width, rect.height → rendered size including borders/paddingBecause the rect is viewport-relative, it changes as you scroll. To get the element's position in document coordinates, add the scroll offset:
function getDocumentCoords(el) {
const rect = el.getBoundingClientRect();
return {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
};
}This pairing — read with getBoundingClientRect(), add scrollX/Y when you need page coordinates — is the conversion you will reach for constantly.
Finding the element at a point: elementFromPoint()
The reverse question — "what is under these coordinates?" — is answered by document.elementFromPoint(x, y), which takes viewport coordinates (matching clientX/Y) and returns the topmost element there, or null if the point is outside the viewport.
document.addEventListener('click', (event) => {
const el = document.elementFromPoint(event.clientX, event.clientY);
console.log('You clicked on:', el.tagName);
});A common gotcha: pass it pageX/pageY on a scrolled page and it returns the wrong element (or null), because it expects viewport coordinates, not document ones.
Manipulating element positions using coordinates
You can combine these pieces to move things around. The getBoundingClientRect() method lets us calculate the offset between the pointer and the element's corner so the element does not "jump" to the cursor when dragging starts. Here is a square you can drag around:
Example: Draggable HTML element
<!-- snippet: html-result -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Draggable Element Example</title>
<style>
#draggable {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
cursor: pointer;
}
</style>
</head>
<body>
<div id="draggable"></div>
<script>
const elem = document.getElementById('draggable');
let shiftX, shiftY;
function onMouseDown(event) {
shiftX = event.clientX - elem.getBoundingClientRect().left;
shiftY = event.clientY - elem.getBoundingClientRect().top;
function moveAt(pageX, pageY) {
elem.style.left = pageX - shiftX + 'px';
elem.style.top = pageY - shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.clientX + window.scrollX, event.clientY + window.scrollY);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', function stopDrag() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', stopDrag);
elem.removeEventListener('mousedown', onMouseDown);
});
}
elem.addEventListener('mousedown', onMouseDown);
elem.addEventListener('dragstart', function() { return false; });
</script>
</body>
</html>For better performance, consider using transform: translate() instead of left / top for animations, as it avoids layout recalculations. Also, for mobile compatibility, add touchstart, touchmove, and touchend event listeners alongside the mouse events.
Animating with coordinates
You can also drive movement by updating an element's coordinates each frame with requestAnimationFrame. Multiplying by a time delta keeps the speed consistent regardless of the display's refresh rate:
Example: Animated moving object
<!-- snippet: html-result -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Animation Using Coordinates</title>
</head>
<body>
<div id="animateMe" style="width: 50px; height: 50px; background: blue; position: absolute;"></div>
<button id="stopBtn">Stop Animation</button>
<script>
const target = document.getElementById('animateMe');
const stopBtn = document.getElementById('stopBtn');
let pos = 0;
let isRunning = true;
let lastTime = performance.now();
function animate(currentTime) {
if (!isRunning) return;
const delta = (currentTime - lastTime) / 16; // Normalize to ~60fps
lastTime = currentTime;
if (pos >= 350) {
pos = 0;
}
pos += delta;
target.style.left = pos + 'px';
requestAnimationFrame(animate);
}
stopBtn.addEventListener('click', () => { isRunning = false; });
requestAnimationFrame(animate);
</script>
</body>
</html>While JavaScript offers powerful capabilities for creating dynamic and interactive animations, CSS animations are often better suited for simpler animations. CSS animations can provide smoother transitions and are typically more performance-efficient as they are handled by the browser’s rendering engine directly, utilizing less CPU. This makes CSS animations ideal for effects like transitions, fades, and basic movements, especially when high performance and low resource usage are critical.
Conclusion
The key takeaway is that the browser has two coordinate frames: viewport (clientX/Y, getBoundingClientRect(), elementFromPoint()) and document (pageX/Y), and you convert between them by adding or subtracting window.scrollX/Y. Keep those straight and most positioning bugs disappear.
From here, dig into Window Sizes and Scrolling, Scrolling for moving the viewport, and Mouse Events Basics for the events that deliver these coordinates.