Skip to content

Instantly share code, notes, and snippets.

@jlong
Last active July 18, 2022 11:03
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jlong/eff01958791d3e0bf10c to your computer and use it in GitHub Desktop.
Save jlong/eff01958791d3e0bf10c to your computer and use it in GitHub Desktop.
Get the offset of an element relative to the viewport and take scrolling and borders into account
function getViewportOffset(element) {
var node = element
, left = node.offsetLeft
, top = node.offsetTop
;
node = node.parentNode;
do {
var styles = getComputedStyle(node);
if (styles) {
var position = styles.getPropertyValue('position');
left -= node.scrollLeft;
top -= node.scrollTop;
if (/relative|absolute|fixed/.test(position)) {
left += parseInt(styles.getPropertyValue('border-left-width'), 10);
top += parseInt(styles.getPropertyValue('border-top-width'), 10);
left += node.offsetLeft;
top += node.offsetTop;
}
node = position === 'fixed' ? null : node.parentNode;
} else {
node = node.parentNode;
}
} while (node);
return { left: left, top: top };
}
Copy link

ghost commented Jun 22, 2019

looks fine at first,
but sometimes it goes up the DOM-tree up too far (the document-object, [above document.documentElement]) which can not be used with getComputedStyle,
that will throw an error of Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'. ,
The second thing I've discovered is that sometimes scrollLeft and/or scrollTop are undefined, which will turn your collected left and top values to NaN...

The following is pretty much the code above, but with those, and several other fixes (plus, I too love 'comma-first' syntax :])

The code is overly explicit, and commented-in on several issues I've thought it is best to explain,
especially for myself in-case I would need to use it some time from now...

function get_offset_of_element_in_viewport(element){ "use strict";
  var offset = {"left":0,"top":0};  //keep initial values number so you won't get NaN on first math. operation (on undefined).

  for( ; null !== element ; ){
    var style     = undefined
       ,position  = undefined
       ;

    offset.left = offset.left + element.offsetLeft;
    offset.top  = offset.top  + element.offsetTop;

    try{
    style = self.getComputedStyle(element);
    }catch(err){}
    if("undefined" === typeof style) return offset;      //sometimes we can go up to the document (not document.documentElement a.k.a HTML-tag but the actual document-object - then getComputedStyle will fail.

    if(null === element.parentNode)  return offset;      //element is-not contained.

    element  = element.parentNode;                      //element is contained. also - that's our "loop++".
    offset.left = offset.left - (element.scrollLeft || 0);  //sometimes scrollLeft is undefined (that will prevent NaN in the offset.left).
    offset.top  = offset.top  - (element.scrollTop  || 0);  //sometimes scrollLeft is undefined (that will prevent NaN in the offset.top).
    
    position = style.getPropertyValue("position");
    if(true === /relative|absolute|fixed/i.test(position)){
      offset.left = offset.left + (Number.parseInt(style.getPropertyValue("border-left-width"), 10) || 0);
      offset.top  = offset.top  + (Number.parseInt(style.getPropertyValue("border-top-width"),  10) || 0);
    }
    
    element = (true === /fixed/i.test(position)) ? null : element.parentNode; //skip going-up more, in-case the element has a fixed position, in-that-case we're done (the loop stop-condition will be activated).
  }
  
  return offset;
}


console.log(
  get_offset_of_element_in_viewport(
    document.querySelector('input[class="label-input-label"]')   //in test page:  https://closure-compiler.appspot.com/home
  )
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment