getComputedStyle
is one of a kind. It does couple of interesting things in the background and has it's own unique way of using. In this post I'll be sharing my experience and thoughts on this bittersweet API.
I expect myself to edit this post since I interact with this function a lot nowadays. I also think there can be loads of niche cases for it too. By the way I'll put some links that helped me on my journey at the end of the post.
What is getComputedStyle?
Directly from MDN:
The Window.getComputedStyle() method returns an object containing the values of all CSS properties of an element, after applying active stylesheets and resolving any basic computation those values may contain.
Individual CSS property values are accessed through APIs provided by the object, or by indexing with CSS property names.
Usage example in plain JavaScript:
const element = document.getElementById("box");
// Get the computed styles of an element
const styles = getComputedStyle(element);
Simply put it returns styles for the given element. Interesting bit here is resolving computations. Say you have given width
property in CSS with calc()
:
#box {
width: calc(100px - 80px);
}
It'll give you 20px as a result. Meaning you can actually access results of CSS calculations from JavaScript. It even calculates viewport and percentage units (100vh, 50% etc.) and gives the result in pixels. Awesome!
GOTCHA: Computed value is dependent on properties and it's not static. Meaning you can't expect it to calculate something like this:
#box {
--box-width: calc(100px - 80px);
width: var(--box-width);
}
// boxWidth variable will be set to string `calc(100px - 80px)`
// Not `20px`
const boxWidth = getComputedStyle(element)["--box-width"];
This totally makes sense since it'd be impossible to calculate the result of a CSS variable statically. It depends on environment and the property aspects (Think percentages for example).
However you'll have 20 pixels if you try to access the width
property. It'd end up calculating width for the specific element:
// 20px
const { width } = getComputedStyle(element);
That's cool and all but what are the actual use cases?
Transitioning an element from 0px to auto
If you're reading this post after calc-size() is widely available on modern browsers, stop and go use it. It'll likely outperform our solution here. If you're stuck in a reality where we can't transition to auto
, keep going!
In your journey of web development, you might've encountered with animating to/from auto
couple of times. An animation library might suit here well but what if I tell you there's no need for it at all?
Let's say there's a box that we can toggle on/off which has some text content. The text content will be dynamic so we can't give it a max-height
beforehand. Oh the designer also want an animation there, it must transit from 0px to auto height slowly.
The following will be our HTML and CSS:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<style>
#box {
position: relative;
display: block;
width: fit-content;
height: 0px;
overflow: hidden;
background-color: rebeccapurple;
color: whitesmoke;
transition: 1s;
}
</style>
<body>
<button type="button" id="toggle">toggle</button>
<div id="box"></div>
<script type="module" src="/src/main.ts" defer></script>
</body>
</html>
In scripting side, we won't be doing much for the start. We just need to keep a simple state to put or remove the text content. (Whether or not using TS is up to you):
// get the wrapper div
const box = document.getElementById("box") as HTMLDivElement;
// get the trigger button
const button = document.getElementById("toggle") as HTMLButtonElement;
// state that keeps track
let toggle = false;
function changeBoxContent() {
if (toggle) {
box.innerText = `Lorem ipsum dolor sit,
amet consectetur adipisicing elit.
Minima culpa ipsum quibusdam quia
dolorum excepturi sequi non optio,
ad eaque? Temporibus natus eveniet
provident sit cum harum,
praesentium et esse?`;
} else {
box.innerText = "";
}
}
button.addEventListener("click", () => {
// flip the toggle
toggle = !toggle;
// update the content
changeBoxContent();
// ...
});
The code here simply sets box
's innerText
to some psudo string or reverts it back to an empty string.
If you click the button now, you'll see nothing has changed. That's because we set box's height to 0 pixels and hide it's overflow.
In order to know how much space we need for the text, we can set box
's height to auto
and call getComputedStyle
on our box
. The idea here is to let the element enlarge as much as it need via auto
and getComputedStyle
here will fetch us that size in pixels.
button.addEventListener("click", () => {
// flip the toggle
toggle = !toggle;
// update the content
changeBoxContent();
// set the element's height to `auto`, this enlarges the element to fit it's content
box.style.height = "auto";
// we got our desired height property!
const height = getComputedStyle(box).height;
console.log(height);
});
You should see the box had as much height as it needed when you click the button. We can also see the height
in pixels in console:
That's cool but we're not transitioning for sure.
Since we know the height we need, maybe we can set box's height back to where it was. Within a call to requestAnimationFrame
, we can set it to height
we had from getComputedStyle
. Let's try it!
button.addEventListener("click", () => {
// flip the toggle
toggle = !toggle;
// update the content
changeBoxContent();
// set the element's height to `auto`, this enlarges the element to fit it's content
box.style.height = "auto";
// we got our desired height property!
const height = getComputedStyle(box).height;
// set the height back to where it was (0px)
box.style.height = "";
// set the final height in next animation frame
requestAnimationFrame(() => {
box.style.height = height;
});
});
Now that we've set the height to 0px
and changing it in the next animation frame, we should see it animating, right?
Well, not exactly. If you're running this in Chrome in a high end PC, you should observe the transition animation BUT in lower end PCs or in some browsers (like Firefox), you can see nothing has changed.
Comparison of Firefox and Chrome on my computer:
The reason why this is happening is because the calculation for layout happens after the styles. We can't guarantee our layout calculation will happen before the style calculation. Also browsers have different implementations of requestAnimationFrame
too, it seems. (You can learn more about it here)
Don't get into despair though! We have multiple solutions for this. We can:
- Force the browser to run layout calculations immediately before styles
- Use intertwined
requestAnimationFrame
s to make sure the animation will be run on the next tick (also known asdouble rAF()
)
Let's try forcing the browser to calculate styles first. Remember getComputedStyle
? I'm sure you do. It'll come to our rescue here too!
Right after we set height
back to 0px
, we'll force to recalculate the layout:
button.addEventListener("click", () => {
// flip the toggle
toggle = !toggle;
// update the content
changeBoxContent();
// set the element's height to `auto`, this enlarges the element to fit it's content
box.style.height = "auto";
// we got our desired height property!
const height = getComputedStyle(box).height;
// set the height back to where it was (reset)
box.style.height = "";
// we're synchronously forcing the browser to recalculate the height of the element
getComputedStyle(box).height;
// set the final height in next animation frame
requestAnimationFrame(() => {
box.style.height = height;
});
});
GOTCHA: You might be thinking why we're accessing height
property but not assigning it to anything. Well that's because getComputedStyle
computes a property on access. It's actually to make it more optimized, it'll only run layout calculations on access to top
, left
, bottom
, right
, width
and height
. Its not documented but good to keep in mind. Try changing it to getComputedStyle(box)
, you'll see nothing has changed.
So that was one way to solve it and honestly I like this way much better. It's good to know double rAF()
trick too, though.
For that we simply need to wrap our requestAnimationFrame
with another requestAnimationFrame
:
button.addEventListener("click", () => {
// flip the toggle
toggle = !toggle;
// update the content
changeBoxContent();
// set the element's height to `auto`, this enlarges the element to fit it's content
box.style.height = "auto";
// we got our desired height property!
const height = getComputedStyle(box).height;
// set the height back to where it was (reset)
box.style.height = "";
// set the final height in next animation frame
requestAnimationFrame(() => {
requestAnimationFrame(() => {
box.style.height = height;
});
});
});
Think of it like in the next frame, we've queued an animation that'll run on the next frame. I know it sounds weird but that used to solve lots of problems since it has a bug in Chrome too.
That's the wrap! As you can see in a modern world where we have WAAPI, transitions and getComputedStyle
still have their use! It's a bit nasty to understand at start but it has it's own way of doing things, what can I say!
Sources:
Transition to Height Auto With Vue.js by Markus Oberlehner
Need cheap paint? Use getComputedStyle().opacity by Webventures
Top comments (5)
Thank you much for getting into that details!
I suppose, layout calculations can be tricky in real world cases, especially if modern frameworks or CSS-in-JS-solutions are involved. Is there a reliable way/event to be sure that the layout is setteled?
You're welcome!
Indeed, they can be. Most performance issues I encounter in real world scenarios are mostly related to layout. I believe you can utilize MutationObserver and ResizeObserver for tracking such events.
Referring to the documentation the "readyState"-event should do the trick. I´m just not sure if it does.
If I use Javascript to change an image, it does not change the readyState:
Nice, I learned stuff :)
Glad you've found it useful!