35

getBoundingClientRect() returns the coordinates of an element on the screen after being transformed. How do I calculate those coordinates before being transformed? i.e. without transforms.

The simplest way I found was:

element.style.transform = 'none'; //temporarily reset the transform
var untransformedOffset = element.getBoundingClientRect().top; //get the value
element.style.transform = ''; //set it back

but that causes slow layout thrashing, especially noticeable if done on many elements. Live demo: http://jsbin.com/nibiqogosa/1/edit?js,console,output

Is there a better way?


That javascript code can be applied to:

<div id="element"></div>
<style> #element { transform: translateY(20px); }</style>

And the result will be 0 (excluding the page's margin)

The result of element.getBoundingClientRect().top will be 20 (excluding the page's margin)

Edit: Answers roundup

http://jsbin.com/kimaxojufe/1/edit?css,js,console,output

fregante
  • 29,050
  • 14
  • 119
  • 159

6 Answers6

23

Get element position without considering any transformation on the element and up the DOM tree :

var el = element,
offsetLeft = 0,
offsetTop  = 0;

do{
    offsetLeft += el.offsetLeft;
    offsetTop  += el.offsetTop;

    el = el.offsetParent;
} while( el );

Get element position without considering transformation applied to it but keeping any transformation up the DOM tree.

To do so you could try to revert the transform.
You must first set transform-origin to 0,0,0 and surround yourself your transformation (scale, rotate) width translate(50%,50%) ... translate(-50%, -50%). Here is an example,
change that :

transform: scale(2) rotate(45deg) translate(20px);
transform-origin: 50% 50%; //default value

into

transform: translate(50%, 50%) scale(2) rotate(45deg) translate(-50%,-50%) translate(20px);
transform-origin: 0 0 0;

We need to do that because the matrix returned by getComputedStyle() does not include stuff done with transform-origin. Don't know really why.

Then you can use this code :

function parseTransform(transform){
    //add sanity check
    return transform.split(/\(|,|\)/).slice(1,-1).map( function(v){
        return parseFloat(v);
    });
}

function convertCoord(transformArr, x, y, z){
    //add sanity checks and default values      

    if( transformArr.length == 6 ){
        //2D matrix
        //need some math to apply inverse of matrix
        var t = transformArr,
            det = t[0]*t[3] - t[1]*t[2];
        return {
            x: (  x*t[3] - y*t[2] + t[2]*t[5] - t[4]*t[3] )/det,
            y: ( -x*t[1] + y*t[0] + t[4]*t[1] - t[0]*t[5] )/det
        }
    }
    else /*if (transformArr.length > 6)*/{
       //3D matrix
       //haven't done the calculation to apply inverse of 4x4 matrix
    }
}

var elRect = element.getBoundingClientRect(),
    st = window.getComputedStyle(element),

    topLeft_pos = convertCoord(
              parseTransform( st.transform ),
              elRect.left,
              elRect.top,
              st.perspective
    );    

I won't explain the math part because I think it's beyond the scope of this post. Could still explain it somewhere else (another question maybe ? ).

Ghetolay
  • 3,222
  • 2
  • 30
  • 29
  • Reverting the transform is a tad too much, but your first solution works perfectly and efficiently! – fregante Oct 05 '16 at 03:38
  • 1
    I didn't understand your question correctly, I thought you just wanted to revert the element transformation but not the parent's transformation if any. I've updated my answer to explain a bit more the difference between the 2 solutions. – Ghetolay Oct 05 '16 at 09:03
  • Yeah maybe it was a big vague. The title mentioned "gBCR without (any) transforms" while the body showed an example with just reverting one transform. • For this reason I create the ["roundup"](http://jsbin.com/kimaxojufe/1/edit?css,js,console,output) to include all the possible situations. • Your first solution solves my _title question_ (in all cases); Benjamin's solves the one-level-only transform (in all cases) – fregante Oct 05 '16 at 09:56
  • See my below answer - the JS method of inverting the matrix is clever, but gets the order of operations wrong and therefore tries harder than it needs to and generates incorrect edge positions when scale is used. – Adam Leggett Sep 10 '19 at 19:10
9

The answer above that inverts the transform mathematically is a nice try, but not quite correct (and more complex than it needs to be). A more correct inversion is below.

This doesn't account for skew or rotate translations, but at least it produces correct edge positions when scale is used and does not impose much of a performance penalty when there is no transform.

It produces accurate results even with scale(0) (albeit losing subpixel precision on width/height).

Note that iOS with the software keyboard open produces different results between getBoundingClientRect() and offsetTop/offsetLeft - and the latter do not support subpixel precision on any browser. This produces results consistent with getBoundingClientRect().

function adjustedBoundingRect(el) {
  var rect = el.getBoundingClientRect();
  var style = getComputedStyle(el);
  var tx = style.transform;

  if (tx) {
    var sx, sy, dx, dy;
    if (tx.startsWith('matrix3d(')) {
      var ta = tx.slice(9,-1).split(/, /);
      sx = +ta[0];
      sy = +ta[5];
      dx = +ta[12];
      dy = +ta[13];
    } else if (tx.startsWith('matrix(')) {
      var ta = tx.slice(7,-1).split(/, /);
      sx = +ta[0];
      sy = +ta[3];
      dx = +ta[4];
      dy = +ta[5];
    } else {
      return rect;
    }

    var to = style.transformOrigin;
    var x = rect.x - dx - (1 - sx) * parseFloat(to);
    var y = rect.y - dy - (1 - sy) * parseFloat(to.slice(to.indexOf(' ') + 1));
    var w = sx ? rect.width / sx : el.offsetWidth;
    var h = sy ? rect.height / sy : el.offsetHeight;
    return {
      x: x, y: y, width: w, height: h, top: y, right: x + w, bottom: y + h, left: x
    };
  } else {
    return rect;
  }
}

var div = document.querySelector('div');
console.log(div.getBoundingClientRect(), adjustedBoundingRect(div));
div.classList.add('transformed');
console.log(div.getBoundingClientRect(), adjustedBoundingRect(div));
.transformed {
  transform: translate(8px,8px) scale(0.5);
  transform-origin: 16px 16px;
}
<div>Hello</div>
Adam Leggett
  • 3,714
  • 30
  • 24
2

I liked Ghetolay's answer. I used it but I made it a bit more performant by avoiding the loop.

I have a draggable tag cloud and I have to update the drag position using transforms, but keep track of the original position (without transform).

The previous answer suggested to loop thru the offsetParents. In my case, and I think in a lot of cases, the tags are transformed but the container isn't. So I only have to get the first offsetParent and use getBoundingClientRect() there. No need to keep looping. I solved it doing this:

var el = element;
var parentRect = element.offsetParent.getBoundingClientRect();
var offsetLeft = parentRect.left + element.offsetLeft;
var offsetTop = parentRect.top + element.offsetTop;
ezakto
  • 3,174
  • 22
  • 27
  • Doesn't look right. The output should be 8 but is 36 http://jsbin.com/nufusupeji/edit?js,console,output – fregante Oct 03 '16 at 20:39
  • You're using `transformed.getBounding...`, You have to use `transformed.offsetParent.getBounfing...`. And, in this case, the parent offset is the body as there's no relatively-positioned wrapper in between, so this method will also count the body margin. So, offsetParent.top is 8 and the element.offsetTop is also 8. If you have no wrapper in between, you can just use element.offsetTop. – ezakto Oct 04 '16 at 04:12
  • I made a proper test suite to test this because there are a few variables and possible desired behaviors (i.e. positioned parents, nested transforms) and yours still seems to be off in many cases. http://jsbin.com/kimaxojufe/1/edit?css,js,console,output Can you take a look and make the necessary change? – fregante Oct 04 '16 at 10:15
  • It doesn't work in any situation. Did you look at the link? The number *never* matches the "base" – fregante Oct 05 '16 at 03:28
  • Same as before. There's a top margin pushing the body 10px down. This method counts that, as there's no wrapper in between. If, for example, `section` tags were `position:relative`, it could compute that right. Again, this method needs a relatively-positioned container. – ezakto Oct 05 '16 at 07:22
2

Just wrap the content of your element by a single div and apply the transformations to it:

<div id="element"><div class="content"></div></div>
<style> #element .content { transform: translateY(20px); }</style>
<script>
    var element = document.getElementById("element");
    var transformedOffset = element.children[0].getBoundingClientRect().top;
    var untransformedOffset = element.getBoundingClientRect().top;
</script>
maxime schoeni
  • 2,666
  • 2
  • 18
  • 19
  • 1
    This is probably best answer if you can do that. Sometimes you only want to measure things but it's not practical to change the DOM. – fregante Aug 18 '22 at 09:35
0

Improve Ghetolay's answer:

Get element position without considering any transformation on the element and up the DOM tree :

function getOffset(element)
{
  var offsetLeft = 0;
  var offsetTop  = 0;
  
  while (element)
  {
      offsetLeft += element.offsetLeft;
      offsetTop  += element.offsetTop;
  
      element = element.offsetParent;
  }
  
  return [offsetLeft, offsetTop];
}

Improvements:

  • Contained in a function
  • loop will handle empty elements without error (element = null/undefined)
DarkTrick
  • 2,447
  • 1
  • 21
  • 39
0

Yes, it is easy:

  1. Get matrix from computed styles transform.
  2. Get transformOrigin from computed styles and offsetWidth plus offsetHeight.
  3. Now we need to apply transformation for box using transform origin as virtual center of coordinates.
  4. Apply transform for points -toX, -toY, -toX + offsetWidth, -toY, -toX, -toY + offsetHeight, -toX + offsetWidth, -toY + offsetHeight.
  5. Find minimum x and y for 4 points above (minX, minY). This point represents left top corner of bounding box in coordinates originated from transform origin.
  6. Now we need to get bounding box using getBoundingClientRect from the coordinates originated from document 0, 0. x and y represents coordinates for the same top left corner of bounding box, but in coordinates originated from document 0, 0.
  7. Transform origin point in coordinates originated from document 0, 0 will have rect.x - minX, rect.y - minY coordinates.
  8. Position of the box will be rect.x - minX - toX, rect.y - minY - toY.

The amount of code is not applicable for stackoverflow. But I really prefer this method, because sum of offsetLeft/offsetTop is not reliable across different web browsers and different styles, it may be affected by scrollbar, etc. You can just open jquery source code and find how position function looks like (including all its dependencies). This method replaces it.

puchu
  • 3,294
  • 6
  • 38
  • 62