Coordinates

  1. Coordinate systems
  2. Element coordinates by offsetParent
  3. The right way: elem.getBoundingClientRect
  4. The combined approach
  5. Summary

There are two coordinate systems in the browser.

  1. relative to document - the zero point is at the left-upper corner of the page.
  2. relative to window - the zero point is at the left-upper corner of the current visible area.

Coordinate systems

When the page is not scrolled, window and document coordinates are the same and share the zero point:

After the scroll, the visible area moves from the document start:

Actually, it is easy to transform between these coordinate systems. Document coordinates are window coordinates plus scroll.

Most of time, only document coordinates are used, because they remain same after scrolling.

Element coordinates by offsetParent

Element coordinates are the coordinates of the left-upper corner. There is unfortunately no single property which gives coordinates. But they can be calculated using offsetTop/offsetLeft and offsetParent.

A natural (but as we’ll see, a buggy) way of calculating absolute coordinates is to traverse up the offsetParent chain and sum offsetLeft/offsetTop, like this:

function getOffsetSum(elem) {
  var top=0, left=0

  while(elem) {
    top = top + parseInt(elem.offsetTop)
    left = left + parseInt(elem.offsetLeft)
    elem = elem.offsetParent        
  }
   
  return {top: top, left: left}
}

There are two main downsides of this approach.

  1. It is buggy. Different browsers have different pitfalls. There are problems with taking borders and scrolls into account.
  2. It is slow. Every time we have to go through whole chain of offsetParents.

It is possible to write a cross-browser code with all the bugs fixed, but let’s review an alternative solution which is supported by Internet Explorer 6+, Firefox 3+ и Opera 9.62+, and modern Safari/Chrome too.

The right way: elem.getBoundingClientRect

This method is described in W3C standard, and most modern browsers implement it (IE too).

It returns a rectangle which encloses the element. The rectangle is given as an object with properties top, left, right, bottom.

The four numbers represent coordinates of the top-left and right-bottom corners. For example, click on the button below to see it’s rectangle:

<input id="brTest" type="button" value="Show button.getBoundingClientRect()" onclick='showRect(this)'/>

<script>
function showRect(elem) {
  var r = elem.getBoundingClientRect()
  alert("Top/Left: "+r.top+" / "+r.left)
  alert("Right/Bottom: "+r.right+" / "+r.bottom)
}
</script>
**The coordinates are given relative to `window`, not the document**. For example, if you scroll this page, so that the button goes to the window top, then its `top` coordinate becomes close to `0`, because it is given relative to window. To calculate coordinates relative to the document that, we need to take page scroll into account.
What is `elem.getBoundingClientRect()`?
Following CSS specification, any content is enclosed by the rectangle called a *CSS box*. In case of block element, like `DIV` - the element itself forms such a rectangle. Such rectangle is called a *block box*. But if an element is inline and contains long text, it requires multiple rectangles to show up. Every line is a rectangle. Such rectangles are called *anonymous boxes*. This stuff is described in great details in CSS specification: http://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level". So, the element contents can be in single or multiple rectangles. It is possible to get all these rectangles by calling `elem.getClientRects()`. It works fine excepts for IE<8 which returns non-standard rectangles, but anyway we don't call `getClientRects` directly. The method `elem.getBoundingClientRect()` returns a single minimal rectangle which encloses all boxes returned by `getClientRects()`.
Let's make a new version of coordinate calculator using `getBoundingClientRect`:
function getOffsetRect(elem) {
    // (1)
    var box = elem.getBoundingClientRect()
    
    var body = document.body
    var docElem = document.documentElement
    
    // (2)
    var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop
    var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft
    
    // (3)
    var clientTop = docElem.clientTop || body.clientTop || 0
    var clientLeft = docElem.clientLeft || body.clientLeft || 0
    
    // (4)
    var top  = box.top +  scrollTop - clientTop
    var left = box.left + scrollLeft - clientLeft
    
    return { top: Math.round(top), left: Math.round(left) }
}
The steps are:
  1. Get the enclosing rectangle.
  2. Calculate the page scroll. All browsers except IE<9 support `pageXOffset/pageYOffset`, and in IE when DOCTYPE is set, the scroll can be taken from documentElement(<html>), otherwise from `body` - so we take what we can.
  3. The document (`html` or `body`) can be shifted from left-upper corner in IE. Get the shift.
  4. Add scrolls to window-relative coordinates and substract the shift of `html/body` to get coordinates in the whole document.
For Firefox an additional rounding is sometimes required, that's why `Math.round()` is in. # Comparison of methods In the demo below, there are 3 nested `DIVs`. All of them have `border`, some of them have `position/margin/padding`. A click on the inner div shows absolute coordinates from both methods: `getOffsetSum` and `getOffsetRect`, and also shows real mouse coordinates as `event.pageX/pageY` (we discuss them later in the article [](#108)). All values appear below the `DIVs`.
Click to get my absolute coordinates with`getOffsetSum` and `getOffsetRect`
**getOffsetSum**:value of getOffsetSum()
**getOffsetRect**:value of getOffsetRect()
**mouse**:mouse coordinates

Click anywhere on the yellow div. It will show results for getOffsetSum(elem) and getOffsetRect(elem) below. Note they usually don’t match.

To see which result is correct - click on the very upper-left corner of the yellow element. It is located on the upper-left corner of the black border.

The absolute mouse coordinates will appear so you can compare them with getOffsetSum/getOffsetRect.

Try it to see that getOffsetRect is always right Smile.

The combined approach

Many frameworks use something a combined approach:

function getOffset(elem) {
    if (elem.getBoundingClientRect) {
        return getOffsetRect(elem)
    } else { // old browser
        return getOffsetSum(elem)
    }
}

function getOffsetSum(elem) {
  var top=0, left=0
  while(elem) {
    top = top + parseInt(elem.offsetTop)
    left = left + parseInt(elem.offsetLeft)
    elem = elem.offsetParent        
  }
   
  return {top: top, left: left}
}


function getOffsetRect(elem) {
    var box = elem.getBoundingClientRect()
    
    var body = document.body
    var docElem = document.documentElement
    
    var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop
    var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft
    
    var clientTop = docElem.clientTop || body.clientTop || 0
    var clientLeft = docElem.clientLeft || body.clientLeft || 0
    
    var top  = box.top +  scrollTop - clientTop
    var left = box.left + scrollLeft - clientLeft
    
    return { top: Math.round(top), left: Math.round(left) }
}


function getOffset(elem) {
    if (elem.getBoundingClientRect) {
        return getOffsetRect(elem)
    } else {
        return getOffsetSum(elem)
    }
}

Summary

There are document-based and window-based coordinates. Document-based are scroll-tolerant and are used most of time.

The two methods to calculate coordinates are:

  1. Sum offsetLeft/Top - many browser bugs, not recommended.
  2. Use getBoundingClientRect - works in all recent major browsers, also supported by IE6+.

Tutorial

Donate

Donate to this project