Home  >  Article  >  Web Front-end  >  getComputedStyle: The good, the bad and the ugly parts

getComputedStyle: The good, the bad and the ugly parts

WBOY
WBOYOriginal
2024-07-28 10:42:23751browse

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:

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

Since we know 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:

getComputedStyle: The good, the bad and the ugly parts

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

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

The above is the detailed content of getComputedStyle: The good, the bad and the ugly parts. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn