DEV Community

Cover image for getComputedStyle: The good, the bad and the ugly parts
Halil Durak
Halil Durak

Posted on • Edited on

getComputedStyle: The good, the bad and the ugly parts

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);


Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode


// boxWidth variable will be set to string `calc(100px - 80px)`
// Not `20px`
const boxWidth = getComputedStyle(element)["--box-width"];


Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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();

  // ...
});


Enter fullscreen mode Exit fullscreen mode

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);
});


Enter fullscreen mode Exit fullscreen mode

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:

Result A1

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;
  });
});


Enter fullscreen mode Exit fullscreen mode

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:

Result A2

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 requestAnimationFrames to make sure the animation will be run on the next tick (also known as double 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;
  });
});


Enter fullscreen mode Exit fullscreen mode

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;
    });
  });
});


Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
efpage profile image
Eckehard

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?

Collapse
 
nikneym profile image
Halil Durak

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.

Collapse
 
efpage profile image
Eckehard

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:

    setTimeout(() => {
      let img =document.getElementById("myImage")
      img.src = "xyz.png"
   }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
miketalbot profile image
Mike Talbot ⭐

Nice, I learned stuff :)

Collapse
 
nikneym profile image
Halil Durak

Glad you've found it useful!