Home

Nullify transforms in Node.getBoundingClientRect()

December 21, 2018

There exists a very neat DOM API called Element.getBoundingClientRect() that allows someone to query a couple of crucial metrics for an element on the page, regarding its position (top, left etc. relative to the viewport) and dimensions (width, height).

Fortunately or not, the returned metrics include the changes did by the transformation matrix onto the element, thus allowing you to compute based on the actual image that is seen by user on the screen.

Simply put, the red square from the next demo has an equal witdth and height of 100px both and an applied scale of 1.5. By doing the trivial math, its final dimensions are 150px by 150px. The actual problem is to compute the initial physical dimensions without hard coding the scale in JS.

This gets even more interesting when you put translations and rotations into the mix, as in the next example.

Here we have a translateX(50px) applied to the element. Even if you get to hardcoded those values in code, the computation gets pretty complex very quickly.

Sometimes though, in some very rare cases (you’ll know when you have them), this behavior is not the desired outcome and there is a need to know the initial element position and metrics, as if the CSS transform property was not even applied to it. We want to achieve this by not mutating the element itself, it should remain intact, obviously to not cause any glitches or jumps on the screen.

There is a trick to achieve this by doing some computations around the transformed values and we are going to pack this computation into a neat API similar to getBoundingClientRect().

function nullifyTransforms(el) {
  const parseTransform = el =>
    window
      .getComputedStyle(el)
      .transform.split(/\(|,|\)/)
      .slice(1, -1)
      .map(v => parseFloat(v))

  // 1
  let { top, left, width, height } = el.getBoundingClientRect()
  let transformArr = parseTransform(el)

  if (transformArr.length == 6) {
    // 2D matrix
    const t = transformArr

    // 2
    let det = t[0] * t[3] - t[1] * t[2]

    // 3
    return {
      width: width / t[0],
      height: height / t[3],
      left: (left * t[3] - top * t[2] + t[2] * t[5] - t[4] * t[3]) / det,
      top: (-left * t[1] + top * t[0] + t[4] * t[1] - t[0] * t[5]) / det,
    }
  } else {
    // This case is not handled because it's very rarely needed anyway.
    // We just return the tranformed metrics, as they are, for consistency.
    return { top, left, width, height }
  }
}

Disclaimer: This code is based a lot on this answer from Stackoverflow. It used to include only computing initial x and y values of the element. But the version above provides the solution for computing initial width and height too. Having that in place, is very easy to compute the leftovers (bottom and right) — they can easily be derived from the existing results, when needed.

The logic of it is quite simple, when you try to visually parse it:

  1. We get the initial values (with transforms included) of the needed metrics
  2. The transform matrix is parsed from current CSS values and we compute its determinant
  3. This part is the most involved one, it carefully assembles a formula that reverses all the operations done by each transform operator

Having this figured out, we assemble this trickery into a sweet API with an even more sweeter usage:

const { left, right, width, height } = nullifyTransforms(
  document.querySelector('.box')
)

It can be used in sliders or other complex UIs where you need to know how the element behaved before you applied some changes to it when performing the calculation.

Here’s a small demo with every moving part in place. It shows the metrics before and after the transformation.

Happy transforming!


Andrei Glingeanu

Andrei Glingeanu's notes and thoughts. You should follow him on Twitter, Instagram or contact via email. The stuff he loves to read can be found here on this site or on goodreads.