Coordinates
Any time you want to move something on a page, drop a tooltip next to a button, or figure out which element sits under the mouse, you’re asking a question about coordinates. And the first thing to get straight is that a point on the page doesn’t have one address — it has two.
There are two systems the browser hands you, and they answer different questions:
- Relative to the window — measured from the top-left corner of the visible area. This is the same origin CSS uses for
position:fixed. We’ll write these asclientX/clientY. The name will make more sense later when we get to mouse event properties. - Relative to the document — measured from the top-left corner of the whole document, including whatever has scrolled out of view above and to the left. This matches CSS
position:absoluteanchored to the document root. We’ll write these aspageX/pageY.
When the page sits at the very top, unscrolled, the window’s top-left corner lines up exactly with the document’s top-left corner, so the two systems give identical numbers. Scroll down, though, and they diverge. As content slides upward, the window-relative coordinate of a fixed point shrinks (the point moves closer to the top of the visible area), while its document-relative coordinate never budges — it’s pinned to the document, which doesn’t move under itself.
To restate what changed when the document scrolled down:
pageY, the document-relative coordinate, stayed the same. It’s counted from the document top, which is now scrolled out of sight.clientY, the window-relative coordinate, got smaller — the arrow is shorter — because the point is now nearer the top edge of what you can see.
Element coordinates: getBoundingClientRect
To read the position of a real element, call elem.getBoundingClientRect(). It returns the window coordinates of the smallest rectangle that fully encloses elem, packaged as a built-in DOMRect object.
The core DOMRect properties are:
x/y— coordinates of the rectangle’s origin relative to the window,width/height— the rectangle’s size (these can be negative, more on that below).
On top of those, the browser gives you four derived edge properties for convenience:
top/bottom— the Y-coordinate of the top and bottom edges,left/right— the X-coordinate of the left and right edges.
Here’s a button that reports its own window coordinates. Click it, then scroll the page and click again — you’ll see the vertical numbers (y, top, bottom) shift while the page moves under the window:
<p><input id=“brTest” type=“button” style=“max-width: 90vw;” value=“Measure me with button.getBoundingClientRect()” onclick=‘reportRect(this)’/></p>
<script>
function reportRect(elem) {
let r = elem.getBoundingClientRect();
alert(x:${r.x} y:${r.y} width:${r.width} height:${r.height} top:${r.top} bottom:${r.bottom} left:${r.left} right:${r.right} );
}
</script>
Here’s the same idea, but writing the numbers into the page instead of an alert so you can read them at leisure. Click Read my rect, then scroll the whole lesson page and click again — watch y, top, and bottom change while width, height, left, and right hold steady:
Here’s how the returned rectangle maps onto the page. The origin (x, y) sits at the top-left, and width/height reach out to the far edges:
Since x/y and width/height completely describe the rectangle, the edge properties are just sums:
left = xtop = yright = x + widthbottom = y + height
Two details that trip people up:
- Coordinates can be fractional, like
10.5. The browser does layout math with sub-pixel precision, so this is expected. You don’t need to round before assigning tostyle.leftorstyle.top— CSS handles the fraction fine. - Coordinates can be negative. Scroll an element above the top of the window and its
getBoundingClientRect().topgoes negative, because the element’s top edge is now above the visible area’s origin.
elementFromPoint(x, y)
Sometimes you have coordinates and want the element sitting there. document.elementFromPoint(x, y) returns the most deeply nested element at the given window coordinates.
let elem = document.elementFromPoint(x, y);
For example, this highlights whatever element is currently at the center of the window and prints its tag name:
let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;
let elem = document.elementFromPoint(centerX, centerY);
elem.style.background = "red";
alert(elem.tagName);
Because it works in window coordinates, the answer depends on where the page is scrolled — the same (x, y) can land on different elements at different scroll positions.
Try it directly. Click anywhere inside the panel below — the demo passes your click’s window coordinates to document.elementFromPoint(x, y) and reports the single deepest element sitting there, then outlines it. Notice how clicking the button reports the button, while clicking the padding around it reports a container:
Using coordinates for “fixed” positioning
Most of the time you read coordinates in order to place something. To pop a label or menu next to an element, grab the element’s window rect and feed those numbers into CSS position:fixed with left/top (or right/bottom).
The function below builds a note and positions it directly under elem:
let elem = document.getElementById("coords-anchor");
function placeNoteUnder(elem, html) {
// create note element
let note = document.createElement('div');
// in real code, better to move this styling into a CSS class
note.style.cssText = "position:fixed; color: red";
// assign coordinates, don't forget the "px" unit!
let coords = elem.getBoundingClientRect();
note.style.left = coords.left + "px";
note.style.top = coords.bottom + "px";
note.innerHTML = html;
return note;
}
// Usage: add it to the document for 5 seconds
let note = placeNoteUnder(elem, 'Saved to your list');
document.body.append(note);
setTimeout(() => note.remove(), 5000);
Click to run it:
<button id=“coords-anchor”>Button with id=“coords-anchor”, the note will appear under it</button>
Because you have the full rectangle, variations are easy — nudge the message to the left or right of the element, tuck it above, add a fade-in animation. It’s all arithmetic on coords.
There’s one catch, and it’s important. With position:fixed, the message is glued to the window, not the element. Scroll the page and the button moves up with the content while the message stays parked at its old window position. They drift apart.
To keep the message stuck to the element through a scroll, switch to document-based coordinates and position:absolute.
Document coordinates
Document-relative coordinates count from the top-left corner of the whole document instead of the visible window. In CSS terms, window coordinates match position:fixed, and document coordinates match position:absolute measured from the top of the document.
Give something position:absolute with top/left, and it stays pinned to that spot in the document as the page scrolls — which is exactly the behavior we want for a label that follows its element. You just need the right numbers to start from.
There’s no built-in method that returns an element’s document coordinates directly, but it takes only a few lines to compute. The two systems are linked by the amount of page that’s scrolled off:
pageY=clientY+ the height of the document scrolled out above the window.pageX=clientX+ the width of the document scrolled out to the left of the window.
That scrolled-out amount is precisely what window.pageYOffset and window.pageXOffset report. So getCoords(elem) reads the window rect and adds the current scroll:
// get document coordinates of the element
function getCoords(elem) {
let box = elem.getBoundingClientRect();
return {
top: box.top + window.pageYOffset,
right: box.right + window.pageXOffset,
bottom: box.bottom + window.pageYOffset,
left: box.left + window.pageXOffset
};
}
Swap getBoundingClientRect() for getCoords() and position:fixed for position:absolute, and the note now travels with its element on scroll:
function placeNoteUnder(elem, html) {
let note = document.createElement('div');
note.style.cssText = "position:absolute; color: red";
let coords = getCoords(elem);
note.style.left = coords.left + "px";
note.style.top = coords.bottom + "px";
note.innerHTML = html;
return note;
}
Here you can watch the drift happen for yourself. Two labels are attached just under the same box: the blue one uses document coordinates with position:absolute, the red one uses window coordinates with position:fixed. At rest they line up. Scroll the preview down and the blue label stays glued to the box while the red one is left parked against the viewport:
Summary
Every point on the page carries two coordinates:
- Relative to the window — from
elem.getBoundingClientRect(), givingx/yand the edgestop/right/bottom/left. - Relative to the document — the window rect plus the current page scroll (
pageXOffset/pageYOffset, a.k.a.scrollX/scrollY).
At the top of an unscrolled page the two agree; after scrolling they differ by the scroll offset. Window coordinates go with position:fixed; document coordinates go with position:absolute. Neither is “better” — they suit different jobs, the same way CSS fixed and absolute do. Add in document.elementFromPoint(x, y) to go the other way and find the element sitting at a given window point, and you have everything you need to measure and move things on a page.