Nullify transforms in Node.getBoundingClientRect()
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:
- We get the initial values (with transforms included) of the needed metrics
- The transform matrix is parsed from current CSS values and we compute its determinant
- 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'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. Wanna vent or buy me a coffee?