How to get the X and Y position of an element in JavaScript?
Get an element's position with getBoundingClientRect, convert it to page or parent coordinates, see how offsetTop differs, and avoid slow layout reads.
Call getBoundingClientRect() on the element. It returns the element's position and size relative to the viewport, the visible part of the page:
const card = document.querySelector('.card');
const rect = card.getBoundingClientRect();
rect.top; // px from the top edge of the viewport
rect.left; // px from the left edge of the viewport
rect.width; // rendered width
rect.height; // rendered heightThe result also has x and y, which are the same as left and top, plus right and bottom. Careful: right is left + width and bottom is top + height, both measured from the top-left corner of the viewport, not from the right or bottom edge like the CSS properties.
The values can be fractions and include CSS transforms: a scaled element returns its scaled size, and a rotated one returns the smallest box that contains it. Because they're relative to the viewport, top and left change as the user scrolls.
Position in the document
Add the scroll position to get coordinates from the top-left corner of the page. These stay the same while the user scrolls:
function getPagePosition(el) {
const rect = el.getBoundingClientRect();
return {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
};
}Position relative to another element
Subtract the two rects:
const list = document.querySelector('.list');
const item = list.querySelector('.list-item.is-active');
const listRect = list.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
const top = itemRect.top - listRect.top;
const left = itemRect.left - listRect.left;This measures from the outer edge of the parent's border. If the parent scrolls and you need the position inside its scrolled content, add list.scrollTop and subtract list.clientTop, which is the width of its top border.
The same idea gives you the pointer position inside an element, because clientX and clientY are relative to the viewport too. For example, for a spotlight effect that follows the pointer:
card.addEventListener('pointermove', (event) => {
const rect = card.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
card.style.setProperty('--x', `${x}px`);
card.style.setProperty('--y', `${y}px`);
});event.offsetX looks like a shortcut, but it's relative to the element directly under the pointer, which can be a child of the card.
offsetTop and offsetLeft
el.offsetTop and el.offsetLeft are older properties with a few differences:
- They're relative to the element's
offsetParent: the nearest positioned ancestor, a table cell, orbody. That's not the page and not necessarily the direct parent. - They're rounded to whole pixels.
- They ignore transforms.
offsetParentisnullwhen the element or an ancestor hasdisplay: none.
They're fine for simple layout math, but use getBoundingClientRect() when you need the position on screen.
Scroll to an element below a sticky header
A common use: scroll to a section without hiding its heading under a sticky header.
const header = document.querySelector('.site-header');
const section = document.querySelector('#pricing');
const top =
section.getBoundingClientRect().top +
window.scrollY -
header.offsetHeight;
window.scrollTo({ top, behavior: 'smooth' });The CSS alternative needs no JavaScript. The browser applies scroll-margin-top whenever it scrolls to the element for an anchor link like #pricing or for scrollIntoView():
section[id] {
scroll-margin-top: 5rem; /* the header height */
}Batch reads and writes
Reading getBoundingClientRect(), offsetTop or offsetHeight makes the browser recalculate layout if something changed since the last calculation. Alternating reads and style changes in a loop forces that recalculation on every iteration:
const items = [...document.querySelectorAll('.item')];
// Slow: every read comes right after a write
items.forEach((item) => {
item.style.height = `${item.offsetWidth / 2}px`;
});
// Fast: read everything first, then write
const widths = items.map((item) => item.offsetWidth);
items.forEach((item, i) => {
item.style.height = `${widths[i] / 2}px`;
});Also avoid measuring elements in a scroll listener. To react when an element enters the screen, use IntersectionObserver, as shown in how to check if an element is visible in the viewport.