Skip to main content

How to Work with Element Coordinates in JavaScript

Positioning elements precisely on a web page requires understanding coordinates. Every pixel on the screen has a position, and JavaScript gives you the tools to find where any element is located, what element sits at a specific point, and how to convert between different coordinate systems.

Coordinates are the backbone of interactive UI: placing tooltips next to the element that triggered them, positioning dropdown menus below their toggle buttons, implementing drag-and-drop, detecting what the user clicked on, and drawing overlays that align perfectly with existing content. All of these tasks require you to measure positions accurately.

The challenge is that the browser uses two different coordinate systems, and confusing them is one of the most common sources of positioning bugs. This guide explains both systems, shows you how to measure element positions with getBoundingClientRect(), how to find elements at specific coordinates with elementFromPoint(), and how to convert between window and document coordinates.

Two Coordinate Systems: Window vs. Document

The browser works with two coordinate systems. They share the same origin (top-left corner) when the page is not scrolled, but they diverge as soon as the user scrolls.

Window Coordinates (Client Coordinates)

Window coordinates are measured from the top-left corner of the visible viewport (the browser window area where content is displayed). They are also called client coordinates.

Key characteristics:

  • The origin (0, 0) is the top-left corner of the viewport.
  • They do not change when the page is scrolled. A fixed-position element always has the same window coordinates regardless of scroll position.
  • They correspond to the area you can see right now.
  • CSS position: fixed uses this coordinate system.
    Window Coordinates
┌─────────────────────────────┐
│(0,0) │
│ ┌───────────────────┐ │
│ │ Element │ │
│ │ │ │ ← What the user sees
│ └───────────────────┘ │
│ (clientX, clientY) │
│ │
└─────────────────────────────┘

Document Coordinates (Page Coordinates)

Document coordinates are measured from the top-left corner of the entire document, including the parts that are scrolled out of view. They are also called page coordinates.

Key characteristics:

  • The origin (0, 0) is the top-left corner of the entire document (the very beginning of the page).
  • They change relative to the viewport when the page is scrolled. An element at document position (100, 2000) might be far below the visible area.
  • They represent the absolute position within the full page.
  • CSS position: absolute (relative to the document or a positioned ancestor) conceptually works in this system.
    Document Coordinates
┌──────────────────────────────┐
│(0,0) │
│ │ ← Scrolled out of view (above viewport)
│ │
├──── ── ── ── ── ── ── ── ────┤ ← Top of viewport (scrollY pixels from doc top)
│ │
│ ┌───────────────────┐ │
│ │ Element │ │ ← Visible area
│ └───────────────────┘ │
│ (pageX, pageY) │
├──── ── ── ── ── ── ── ── ────┤ ← Bottom of viewport
│ │
│ │ ← Scrolled out of view (below viewport)
│ │
└──────────────────────────────┘

The Relationship Between the Two Systems

The conversion between the two systems is simple and depends on the current scroll position:

documentX = windowX + window.scrollX
documentY = windowY + window.scrollY

windowX = documentX - window.scrollX
windowY = documentY - window.scrollY

When the page is not scrolled (scrollX = 0, scrollY = 0), both coordinate systems are identical. As the user scrolls down, document coordinates stay fixed (they describe where the element is in the full page), while window coordinates shift (they describe where the element appears on screen).

Here is a concrete example:

// An element is 500px from the top of the document.
// The user has scrolled down 200px.

let documentY = 500; // Always 500, regardless of scroll
let scrollY = 200; // Current scroll position
let windowY = documentY - scrollY; // 300 (the element is 300px from the top of the viewport)

// If the user scrolls down to 600px:
scrollY = 600;
windowY = documentY - scrollY; // -100 (the element is 100px ABOVE the viewport (not visible))

Which System Do JavaScript APIs Use?

Most DOM methods return window coordinates:

APICoordinate System
getBoundingClientRect()Window (client)
event.clientX / event.clientYWindow (client)
event.pageX / event.pageYDocument (page)
elementFromPoint(x, y)Window (client)
CSS position: fixedWindow (client)
CSS position: absoluteRelative to positioned ancestor

Understanding which system each API uses prevents positioning bugs where elements end up in the wrong place.

getBoundingClientRect(): Element Coordinates Relative to Window

The getBoundingClientRect() method is the primary tool for getting an element's position. It returns a DOMRect object with the coordinates of the element's bounding box relative to the viewport (window coordinates).

Basic Usage

<div id="box" style="width: 200px; height: 100px; margin: 50px; padding: 10px; border: 5px solid #333;">
Content
</div>
let box = document.getElementById("box");
let rect = box.getBoundingClientRect();

console.log(rect.x); // e.g., 50 (left edge relative to viewport)
console.log(rect.y); // e.g., 50 (top edge relative to viewport)
console.log(rect.width); // 230 (200 content + 10+10 padding + 5+5 border)
console.log(rect.height); // 130 (100 content + 10+10 padding + 5+5 border)
console.log(rect.top); // e.g., 50 (same as y)
console.log(rect.right); // e.g., 280 (x + width)
console.log(rect.bottom); // e.g., 180 (y + height)
console.log(rect.left); // e.g., 50 (same as x)

DOMRect Properties Explained

The DOMRect object has eight properties:

        left (x)                              right
│ │
▼ ▼
┌────────────────────────────────────┐ ◄── top (y)
│ ▲ │
│ Element │ │
│ │ │
│ ◄ ─────── width ──────────► │
│ height │
│ │ │
│ ▼ │
└────────────────────────────────────┘ ◄── bottom
PropertyDescriptionEquivalent
xLeft edge relative to viewportSame as left
yTop edge relative to viewportSame as top
widthElement's full width (including padding and border)right - left
heightElement's full height (including padding and border)bottom - top
topTop edge relative to viewportSame as y
rightRight edge relative to viewportx + width
bottomBottom edge relative to viewporty + height
leftLeft edge relative to viewportSame as x

The relationships are:

rect.right === rect.left + rect.width   // true
rect.bottom === rect.top + rect.height // true
rect.x === rect.left // true
rect.y === rect.top // true

Coordinates Can Be Negative or Fractional

Since getBoundingClientRect returns window coordinates, the values can be negative when the element is partially or fully scrolled above or to the left of the viewport:

let box = document.getElementById("box");
let rect = box.getBoundingClientRect();

// If the user has scrolled past the element:
console.log(rect.top); // -50 (element is 50px above the viewport)
console.log(rect.bottom); // 50 (bottom edge is still visible)

// Element is fully above the viewport:
console.log(rect.bottom); // -20 (entirely above, not visible)

Values can also be fractional (not integers), especially on high-DPI displays:

console.log(rect.x);      // 49.5
console.log(rect.width); // 230.25
console.log(rect.top); // 49.5

Values Change on Scroll

Because getBoundingClientRect returns window coordinates, the results change as the user scrolls. The element's position in the document does not change, but its position relative to the viewport does:

let box = document.getElementById("box");

// Before scrolling:
console.log(box.getBoundingClientRect().top); // 500

// After scrolling down 200px:
console.log(box.getBoundingClientRect().top); // 300

// After scrolling down 600px:
console.log(box.getBoundingClientRect().top); // -100 (above viewport)

This is exactly the behavior you want for most positioning tasks. If you place a tooltip at the element's getBoundingClientRect() position using position: fixed, it stays aligned with the element on screen.

Practical Use: Positioning a Tooltip

function showTooltip(targetElement, text) {
// Remove existing tooltip
let existing = document.getElementById("tooltip");
if (existing) existing.remove();

// Create tooltip
let tooltip = document.createElement("div");
tooltip.id = "tooltip";
tooltip.textContent = text;
tooltip.style.cssText = `
position: fixed;
background: #333;
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
pointer-events: none;
z-index: 10000;
white-space: nowrap;
`;

document.body.append(tooltip);

// Get target position (window coordinates)
let targetRect = targetElement.getBoundingClientRect();
let tooltipRect = tooltip.getBoundingClientRect();

// Position above the target, centered horizontally
let left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
let top = targetRect.top - tooltipRect.height - 8;

// Keep within viewport bounds
if (left < 4) left = 4;
if (left + tooltipRect.width > window.innerWidth - 4) {
left = window.innerWidth - tooltipRect.width - 4;
}
if (top < 4) {
// If not enough space above, show below
top = targetRect.bottom + 8;
}

// Use position: fixed with window coordinates
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}

function hideTooltip() {
let tooltip = document.getElementById("tooltip");
if (tooltip) tooltip.remove();
}

// Usage
let button = document.querySelector(".help-button");
button.addEventListener("mouseenter", () => showTooltip(button, "Click for help"));
button.addEventListener("mouseleave", hideTooltip);

Because getBoundingClientRect returns window coordinates and the tooltip uses position: fixed, the tooltip aligns perfectly with the target element on screen, regardless of page scroll.

Practical Use: Checking if an Element Is Visible

function isElementInViewport(element) {
let rect = element.getBoundingClientRect();
let viewportWidth = document.documentElement.clientWidth;
let viewportHeight = document.documentElement.clientHeight;

return (
rect.top < viewportHeight &&
rect.bottom > 0 &&
rect.left < viewportWidth &&
rect.right > 0
);
}

function isElementFullyInViewport(element) {
let rect = element.getBoundingClientRect();
let viewportWidth = document.documentElement.clientWidth;
let viewportHeight = document.documentElement.clientHeight;

return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= viewportHeight &&
rect.right <= viewportWidth
);
}

let section = document.getElementById("features");
console.log("Partially visible:", isElementInViewport(section));
console.log("Fully visible:", isElementFullyInViewport(section));

Getting the Center of an Element

function getElementCenter(element) {
let rect = element.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}

let button = document.querySelector("button");
let center = getElementCenter(button);
console.log(`Button center: (${center.x}, ${center.y})`);

Measuring Distance Between Elements

function distanceBetweenElements(elem1, elem2) {
let center1 = getElementCenter(elem1);
let center2 = getElementCenter(elem2);

let dx = center2.x - center1.x;
let dy = center2.y - center1.y;

return Math.sqrt(dx * dx + dy * dy);
}

let boxA = document.getElementById("box-a");
let boxB = document.getElementById("box-b");
console.log(`Distance: ${distanceBetweenElements(boxA, boxB).toFixed(1)}px`);

elementFromPoint(x, y): Element at Coordinates

The document.elementFromPoint(x, y) method takes window coordinates and returns the topmost element at that position. If multiple elements overlap, it returns the one on top (highest z-index or latest in DOM order).

Basic Usage

// What element is at the center of the viewport?
let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;

let centerElement = document.elementFromPoint(centerX, centerY);
console.log(centerElement); // Whatever element is at the center of the screen
console.log(centerElement.tagName); // e.g., "DIV", "P", "SECTION"

Out-of-Window Coordinates

If the coordinates are outside the viewport, elementFromPoint returns null:

// Coordinates outside the visible viewport
let outside = document.elementFromPoint(-10, -10);
console.log(outside); // null

let belowViewport = document.elementFromPoint(100, window.innerHeight + 100);
console.log(belowViewport); // null
caution

elementFromPoint takes window coordinates, not document coordinates. If you have document coordinates, subtract the scroll offset before passing them:

// ❌ Wrong: passing document coordinates directly
let elem = document.elementFromPoint(docX, docY); // Incorrect if page is scrolled

// ✅ Correct: convert to window coordinates first
let elem = document.elementFromPoint(docX - window.scrollX, docY - window.scrollY);

elementsFromPoint(x, y): All Elements at a Point

While elementFromPoint returns only the topmost element, document.elementsFromPoint(x, y) returns an array of all elements at the specified coordinates, ordered from topmost to bottommost:

let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;

let allElements = document.elementsFromPoint(centerX, centerY);
console.log(allElements);
// e.g., [<span>, <p>, <div>, <section>, <body>, <html>]
// From most specific (topmost) to most general (bottommost)

allElements.forEach(el => {
console.log(el.tagName, el.id || el.className || "");
});

Practical Use: Custom Hit Testing

elementFromPoint is useful for implementing custom interaction logic, especially during drag-and-drop operations:

// During drag: find what's underneath the dragged element
function findDropTarget(draggedElement, mouseX, mouseY) {
// Temporarily hide the dragged element so it doesn't block the hit test
draggedElement.style.display = "none";

// Find what's underneath
let target = document.elementFromPoint(mouseX, mouseY);

// Restore the dragged element
draggedElement.style.display = "";

return target;
}

// Usage during a drag operation
document.addEventListener("mousemove", (event) => {
if (!isDragging) return;

let dropTarget = findDropTarget(draggedElement, event.clientX, event.clientY);

if (dropTarget && dropTarget.classList.contains("drop-zone")) {
dropTarget.classList.add("drop-hover");
}
});

Practical Use: Custom Context Menu

document.addEventListener("contextmenu", (event) => {
event.preventDefault();

let clickedElement = document.elementFromPoint(event.clientX, event.clientY);

let menuItems = [];

if (clickedElement.matches("img")) {
menuItems.push("Save Image", "Copy Image", "Open in New Tab");
} else if (clickedElement.matches("a")) {
menuItems.push("Open Link", "Copy Link", "Open in New Tab");
} else if (clickedElement.matches("p, span, h1, h2, h3")) {
menuItems.push("Copy Text", "Select All", "Search");
} else {
menuItems.push("Inspect", "View Source", "Reload");
}

showCustomContextMenu(event.clientX, event.clientY, menuItems);
});

Practical Use: Identifying Elements Under the Cursor

// Highlight whatever element is under the cursor
let highlightBox = document.createElement("div");
highlightBox.style.cssText = `
position: fixed;
border: 2px solid red;
background: rgba(255, 0, 0, 0.1);
pointer-events: none;
z-index: 99999;
transition: all 0.05s;
`;
document.body.append(highlightBox);

document.addEventListener("mousemove", (event) => {
// Hide the highlight temporarily so it doesn't interfere
highlightBox.style.display = "none";
let element = document.elementFromPoint(event.clientX, event.clientY);
highlightBox.style.display = "";

if (element && element !== document.body && element !== document.documentElement) {
let rect = element.getBoundingClientRect();
highlightBox.style.left = `${rect.left}px`;
highlightBox.style.top = `${rect.top}px`;
highlightBox.style.width = `${rect.width}px`;
highlightBox.style.height = `${rect.height}px`;
}
});

Document Coordinates: Calculating from Window + Scroll

JavaScript's DOM APIs primarily return window coordinates. But sometimes you need document coordinates, which are the element's position relative to the entire document (not just the visible viewport). Since there is no built-in method that directly returns document coordinates, you calculate them by adding the current scroll offset to the window coordinates.

The Conversion Formula

Document X = Window X + window.scrollX
Document Y = Window Y + window.scrollY

And the reverse:

Window X = Document X - window.scrollX
Window Y = Document Y - window.scrollY

Getting Document Coordinates for an Element

function getDocumentCoords(element) {
let rect = element.getBoundingClientRect();

return {
left: rect.left + window.scrollX,
top: rect.top + window.scrollY,
right: rect.right + window.scrollX,
bottom: rect.bottom + window.scrollY,
width: rect.width,
height: rect.height
};
}

let box = document.getElementById("box");
let docCoords = getDocumentCoords(box);

console.log(`Document position: (${docCoords.left}, ${docCoords.top})`);
console.log(`Size: ${docCoords.width} x ${docCoords.height}`);

Why Document Coordinates Are Stable

Document coordinates do not change when the user scrolls. This is their primary advantage:

let box = document.getElementById("box");

// Window coordinates change with scroll:
window.addEventListener("scroll", () => {
let windowCoords = box.getBoundingClientRect();
let docCoords = getDocumentCoords(box);

console.log(`Window Y: ${windowCoords.top}`); // Changes as you scroll
console.log(`Document Y: ${docCoords.top}`); // Always the same
});

When to Use Document Coordinates

Use window coordinates when:

  • Positioning elements with position: fixed
  • Using elementFromPoint
  • Working with mouse event clientX/clientY
  • Checking if something is visible in the viewport

Use document coordinates when:

  • Positioning elements with position: absolute (relative to the document)
  • Saving an element's position that should survive scrolling
  • Calculating the absolute distance between elements on the page
  • Creating annotations or markers at fixed document positions

Practical Use: Absolute-Positioned Tooltip

When you use position: absolute instead of position: fixed, you need document coordinates:

function showAbsoluteTooltip(targetElement, text) {
let tooltip = document.createElement("div");
tooltip.className = "tooltip";
tooltip.textContent = text;
tooltip.style.cssText = `
position: absolute;
background: #333;
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
z-index: 10000;
`;

document.body.append(tooltip);

// Get document coordinates (not window coordinates)
let targetCoords = getDocumentCoords(targetElement);

// Position above the target
tooltip.style.left = `${targetCoords.left}px`;
tooltip.style.top = `${targetCoords.top - tooltip.offsetHeight - 8}px`;

// This tooltip scrolls WITH the page, staying aligned with the target
// even without updating on scroll events
}

The key difference: a position: fixed tooltip stays in one place on screen as you scroll (you would need to update its position on scroll), while a position: absolute tooltip (placed with document coordinates) scrolls with the page content naturally, always staying near its target element.

Practical Use: Drawing Connections Between Elements

function drawLineBetween(elem1, elem2, svgContainer) {
let coords1 = getDocumentCoords(elem1);
let coords2 = getDocumentCoords(elem2);

// Center points
let x1 = coords1.left + coords1.width / 2;
let y1 = coords1.top + coords1.height / 2;
let x2 = coords2.left + coords2.width / 2;
let y2 = coords2.top + coords2.height / 2;

let line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", x1);
line.setAttribute("y1", y1);
line.setAttribute("x2", x2);
line.setAttribute("y2", y2);
line.setAttribute("stroke", "#2196f3");
line.setAttribute("stroke-width", "2");

svgContainer.appendChild(line);
}

Converting Event Coordinates

Mouse events provide both coordinate systems directly:

document.addEventListener("click", (event) => {
// Window coordinates (relative to viewport)
console.log(`Window: (${event.clientX}, ${event.clientY})`);

// Document coordinates (relative to full document)
console.log(`Document: (${event.pageX}, ${event.pageY})`);

// You can verify the relationship:
console.log(event.pageX === event.clientX + window.scrollX); // true
console.log(event.pageY === event.clientY + window.scrollY); // true
});

Complete Practical Example

Here is a comprehensive example that demonstrates all coordinate concepts: measuring positions, converting between coordinate systems, finding elements at points, and positioning UI elements:

<!DOCTYPE html>
<html>
<head>
<title>Coordinates Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; min-height: 300vh; padding: 20px; }

.target-box {
width: 200px;
height: 100px;
background: #e3f2fd;
border: 3px solid #2196f3;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #1565c0;
cursor: pointer;
}

#box-a { margin: 200px 100px; }
#box-b { margin: 100px 400px; }
#box-c { margin: 300px 250px; }

.coord-label {
position: absolute;
background: rgba(0,0,0,0.85);
color: #0f0;
font-family: monospace;
font-size: 12px;
padding: 6px 10px;
border-radius: 4px;
pointer-events: none;
z-index: 1000;
line-height: 1.5;
white-space: nowrap;
}

.crosshair {
position: fixed;
pointer-events: none;
z-index: 999;
}

.crosshair-h {
left: 0;
right: 0;
height: 1px;
background: rgba(255, 0, 0, 0.4);
}

.crosshair-v {
top: 0;
bottom: 0;
width: 1px;
background: rgba(255, 0, 0, 0.4);
}

.cursor-info {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.85);
color: #0f0;
font-family: monospace;
font-size: 13px;
padding: 12px 16px;
border-radius: 8px;
z-index: 1001;
line-height: 1.8;
}

.marker {
position: absolute;
width: 12px;
height: 12px;
background: red;
border: 2px solid white;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 500;
}
</style>
</head>
<body>
<h1>Coordinates Demo</h1>
<p>Move your mouse and scroll to see coordinates update in real time.</p>
<p>Click on any element to place a marker at document coordinates.</p>

<div id="box-a" class="target-box">Box A</div>
<div id="box-b" class="target-box">Box B</div>
<div id="box-c" class="target-box">Box C</div>

<!-- Crosshair lines -->
<div class="crosshair crosshair-h" id="crosshair-h"></div>
<div class="crosshair crosshair-v" id="crosshair-v"></div>

<!-- Cursor info panel -->
<div class="cursor-info" id="cursor-info">Move your mouse...</div>

<script>
let crosshairH = document.getElementById("crosshair-h");
let crosshairV = document.getElementById("crosshair-v");
let cursorInfo = document.getElementById("cursor-info");
let labels = [];

// Helper: get document coordinates
function getDocCoords(element) {
let rect = element.getBoundingClientRect();
return {
left: rect.left + scrollX,
top: rect.top + scrollY,
right: rect.right + scrollX,
bottom: rect.bottom + scrollY,
width: rect.width,
height: rect.height
};
}

// Show coordinate labels on target boxes
function updateLabels() {
labels.forEach(label => label.remove());
labels = [];

document.querySelectorAll(".target-box").forEach(box => {
let winRect = box.getBoundingClientRect();
let docCoords = getDocCoords(box);

let label = document.createElement("div");
label.className = "coord-label";
label.innerHTML = `
<strong>${box.id}</strong><br>
Window: (${winRect.left.toFixed(0)}, ${winRect.top.toFixed(0)})<br>
Document: (${docCoords.left.toFixed(0)}, ${docCoords.top.toFixed(0)})<br>
Size: ${winRect.width.toFixed(0)} × ${winRect.height.toFixed(0)}
`;

// Position label using absolute (document) coordinates
label.style.left = `${docCoords.right + 10}px`;
label.style.top = `${docCoords.top}px`;

document.body.append(label);
labels.push(label);
});
}

// Update crosshair and cursor info on mousemove
document.addEventListener("mousemove", (event) => {
// Move crosshairs (fixed position, window coordinates)
crosshairH.style.top = `${event.clientY}px`;
crosshairV.style.left = `${event.clientX}px`;

// Find element under cursor
crosshairH.style.display = "none";
crosshairV.style.display = "none";
let elementUnder = document.elementFromPoint(event.clientX, event.clientY);
crosshairH.style.display = "";
crosshairV.style.display = "";

let elementName = "none";
if (elementUnder) {
elementName = elementUnder.tagName.toLowerCase();
if (elementUnder.id) elementName += `#${elementUnder.id}`;
else if (elementUnder.className) elementName += `.${elementUnder.className.split(" ")[0]}`;
}

// Update info panel
cursorInfo.innerHTML = `
<strong>Cursor Coordinates</strong><br>
Window: (${event.clientX}, ${event.clientY})<br>
Document: (${event.pageX}, ${event.pageY})<br>
Scroll: (${scrollX}, ${scrollY})<br>
Element: ${elementName}<br>
Viewport: ${document.documentElement.clientWidth} × ${document.documentElement.clientHeight}
`;
});

// Place marker on click using document coordinates
document.addEventListener("click", (event) => {
if (event.target.closest(".cursor-info")) return;

let marker = document.createElement("div");
marker.className = "marker";

// Use document (page) coordinates so marker stays in place when scrolling
marker.style.left = `${event.pageX}px`;
marker.style.top = `${event.pageY}px`;

document.body.append(marker);

// Remove marker after 3 seconds
setTimeout(() => marker.remove(), 3000);
});

// Update labels on scroll and resize
window.addEventListener("scroll", updateLabels);
window.addEventListener("resize", updateLabels);

// Initial update
updateLabels();
</script>
</body>
</html>

This example demonstrates:

  • getBoundingClientRect() for reading window coordinates of each box
  • Document coordinate calculation by adding scrollX/scrollY
  • elementFromPoint() for detecting which element is under the cursor
  • Window coordinates for fixed-position elements (crosshairs, info panel)
  • Document coordinates for absolute-position elements (labels, markers)
  • How window coordinates change on scroll while document coordinates remain stable
  • The relationship between clientX/clientY and pageX/pageY on mouse events

Summary

JavaScript uses two coordinate systems for positioning, and understanding both is essential for accurate DOM positioning.

Window (Client) Coordinates:

  • Measured from the top-left corner of the visible viewport.
  • Change as the user scrolls (same element, different coordinates).
  • Used by getBoundingClientRect(), event.clientX/clientY, elementFromPoint().
  • Best for: position: fixed elements, viewport-relative calculations, visibility checks.

Document (Page) Coordinates:

  • Measured from the top-left corner of the entire document.
  • Stable regardless of scroll position.
  • Available on mouse events as event.pageX/pageY.
  • Must be calculated for elements: rect.left + scrollX, rect.top + scrollY.
  • Best for: position: absolute elements, saving positions, document-level calculations.

Key Methods:

Method/PropertyReturnsCoordinate System
element.getBoundingClientRect()DOMRect with x, y, width, height, top, right, bottom, leftWindow
document.elementFromPoint(x, y)Topmost element at that point, or nullWindow
document.elementsFromPoint(x, y)Array of all elements at that pointWindow
event.clientX / event.clientYMouse positionWindow
event.pageX / event.pageYMouse positionDocument

Conversion:

// Window → Document
let docX = windowX + window.scrollX;
let docY = windowY + window.scrollY;

// Document → Window
let winX = docX - window.scrollX;
let winY = docY - window.scrollY;

// Element's document coordinates
let rect = element.getBoundingClientRect();
let docLeft = rect.left + window.scrollX;
let docTop = rect.top + window.scrollY;

Use getBoundingClientRect() as your primary tool for measuring element positions. Use elementFromPoint() for hit testing. Convert to document coordinates when you need positions that are independent of scroll state.