jonelantha: Blog


Moving the goal posts: just how do you find the maximum possible width of an HTML element?

15th July 2020

A look at the intricacies and pitfalls of trying to accurately fill an HTML element with exactly the right amount of content

It turns out that finding the width of an element is pretty straightforward. For some ideas just go to Stack Overflow and search for 'how do I find the width of an html element' - you'll see plenty of suggestions!

So what's this article about? We're looking at the situation where you want to determine the theoretical maximum available width inside an element because you want to exactly fill that element with some content.

Most of the time this is straightforward but when the element has size contraints which make the element want to shrink down to its content (rather than expand up to its container) you'll find that the element's current width is probably going to be less that its maximum width. In simple situations you may be able to calculate the maximum width by looking at the layout but in more complex layouts that won't be practical.

Rather than trying to calculate the maximum width we'll instead focus on content filling strategies. We'll consider the age old problem of how do you accurately fill a <div> with just the right number of animal face emojis: 🐨🐼🐸

(We're using animal emojis to help keep the code simple - to apply this to the real world, think of each animal face as a flex item of fixed width inside a wrapping flexbox container)

Intrisic size constraints

We'll look at an example where the elements have instrinsic size constraints. Before we go any further it's worth understanding what that means. In short it means that the width of the element depends on the content it contains. See the MDN Glossary entry on Intrinsic_Size and if you're feeling brave have a read through the W3 spec: CSS Intrinsic & Extrinsic Sizing Module Level 3

An intrinsic size constraint sounds exotic but it's quite common. It can easily occur with old-school floats or table cells but let's take a look at this flexbox example with two boxes:

<div class="container">
    <div class="orange-box">🐨</div>
    <div class="red-box">🏑</div>
</div>
.container {
    display: flex;
}

.orange-box {
    flex: 1 1 auto;
    border-color: orange;
}

.red-box {
    flex: 1 1 auto;
    border-color: red;
}

Both boxes have flex: 1 1 auto; that effectively means the width of each box is the width of its content plus half of the leftover space (leftover space = width of flexbox container minus content of each box). It's worth taking a moment to think about that; basically the two boxes have both intrinsic and extrinsic influences. Intrinsic because the width of each coloured box depends on its internal content and extrinsic because the leftover space depends on an external factor (i.e. the width of the flex container)

(for more info on flexbox layouts, please see the CSS-TRICKS Flexbox guide)

Let's see what happens when we vary the content in the orange box:

(all examples in this article are inside iframes - you can View Frame Source to see the code)

As you can see the white space in the orange box is always equal to the white space in the red box. In the second example the orange box is much larger because it has more content.

The Problem

So those animals are looking lonely. Let's see how we can horizontally fill the orange box with as many animals as we can.

const animals = Array.from("🐨🦊🐻🐭🐼🐸🐢🐱🐹🐰...");

const singleAnimalWidth = getTextWidth("🐨");

function fillOrangeBoxWithAnimals() {
  const orangeBoxWidth = getContentWidth(orangeBox);

  const numberOfAnimals = Math.floor(orangeBoxWidth / singleAnimalWidth);

  orangeBox.innerText = animals.slice(0, numberOfAnimals).join("");
}

Above is a pretty simple function which attempts to fill the orange box by taking a number of animals from the beginning of the animals array using the simple formula orange box width / width of one animal (a real world formula will be more complex, perhaps allowing for variable sized items).

It probably won't work but it's a starting point... let's see what happens when we use that code with our layout.

Click the button to fill the orange box:

So as expected, that didn't work. It filled the orange box to the width it was initially but after we inserted the animals the orange box grew (because it contained more content). So it looks like the goal posts are moving!

But... try clicking the button a few more times and eventually the orange box should get filled.

Method 1 - if there's still space, have another go

So the above was never going to work but the orange box did get filled after we clicked a few times. Perhaps we should try and incorporate that idea into a loop. Something like this perhaps:

function fillOrangeBoxWithAnimalsIteratively() {
  while (true) {
    const widthBefore = getContentWidth(orangeBox);

    const numberOfAnimals = Math.floor(widthBefore / singleAnimalWidth);

    orangeBox.innerText = animals.slice(0, numberOfAnimals).join("");

    const widthAfter = getContentWidth(orangeBox);

    // width isn't changing anymore so our work is done
    if (widthBefore === widthAfter) break;

    // otherwise go round again and add animals to fit the new width
  }
}

So this loop has the same effect of clicking multiple times and we bail out of the loop once the orange box has stopped growing.

Let's try the above code out. Try clicking the button again:

Much better. And not at all 'janky' as we may have feared! We are forcing the browser to layout that <div> 4 or 5 times (depending on your screen width) but the browser only 'paints' the changes to the screen after the loop has finished running - so thankfully we don't see lots of flickering.

But wait... it looks like we could insert at least one more animal by stealing some of the free space from the red box. It turns out this is one of the limitations of this method, it only fills up to the available space in the orange box, not the true maximum space available. This in turn leads to some strange edge cases - but most of the time this method should be pretty reliable.

Method 2 - Force them in until they wrap

So what if we really want to use all the space available? We can use a different strategy where we keep adding animals until they wrap onto two lines. Here's the code:

const orangeBoxSpan = document.querySelector("#orange-box > span");

function fillOrangeBoxWithAnimals() {
  let widestNonWrappingAnimals = "";

  for (let i = 1; i <= animals.length; i++) {
    const animalIteration = animals.slice(0, i).join("");

    orangeBoxSpan.innerText = animalIteration;

    if (isWrapping(orangeBoxSpan)) {
      break;
    } else {
      widestNonWrappingAnimals = animalIteration;
    }
  }

  orangeBoxSpan.innerText = widestNonWrappingAnimals;
}

function isWrapping(inlineElement) {
  return inlineElement.getClientRects().length > 1;
}

So we loop round and keep a note of the largest combination which didn't wrap. Once the loop finishes (because either we ran out of animals or the animals wrapped onto two lines) we then apply the last non-wrapping combination.

Note isWrapping is a handy helper function which can easily detect if content is wrapping - there's one small restriction though: it only works on inline elements. That's why we've inserted a <span> inside the orange box and we're putting the animals inside that <span>:

<div id="container" class="box-container">
    <div id="orange-box"><span>🐨</span></div>    <div id="red-box">🏑</div>
</div>

Let's see this in action. Once again click the button to fill the orange box with animals:

Perfect!

This second filling strategy produces consistent results but the downside is the browser will perform a layout update for every animal we're showing. That could be as many as 18 updates in this example! That could cause performance issues (depending on the layout and the browser).

(some optimisation should be possible by best guessing the number of animals but we won't cover that here)

Couldn't we just work out the maximum width of the orange box by subtracting off the width of that house emoji?

Yes it's true, we could figure out the maximum width of the orange box by looking at the width of the container and subtracting off the width of the red box's content along with the width of the borders, the padding etc - but these calculations can get complex and difficult to maintain, hence we're looking at solutions which make no assumptions about the element's context.

Summing up

So why is this useful? In front-end development we should no longer be trying to hard code dimensions based on break points; there are just too many variables. Flexbox and CSS Grid should be the tools of choice but there are still a handful of situations where you may need to crack out the JavaScript.

If you do need to use JavaScript to assess width, ideally you should opt for a layout which is purely extrinsic (a layout which depends only on the surrounding elements, not the contained elements). That way you'll only need to grab the width of the element once and then just keep an eye on any size changes via ResizeObserver.

However, there are times where you may not have the luxury of choosing the layout (particularly if the css is not something you have control of). If that's the case, Method 1 has better performance (due to fewer updates) but it may not always produce consistent results. Method 2 will always use the most space available but has the potential to be a little heavy on the CPU.

Hope you've found this useful - happy finding those maximum widths! πŸ™‚

Appendix - Helper methods

The samples above make use of the following helper methods:

function getContentWidth(element) {
  const styles = getComputedStyle(element);
  if (styles.boxSizing === "content-box") {
    return parseFloat(styles.width);
  } else {
    return (
      parseFloat(styles.width) -
      parseFloat(styles.borderLeftWidth) -
      parseFloat(styles.borderRightWidth) -
      parseFloat(styles.paddingLeft) -
      parseFloat(styles.paddingRight)
    );
  }
}

function getTextWidth(text) {
  const div = document.createElement("div");
  div.innerText = text;
  div.style.display = "inline-block";
  div.style.position = "absolute";

  document.body.append(div);

  const width = Math.ceil(getContentWidth(div));

  div.remove();

  return width;
}

Β© 2003-2020 jonelantha