DEV Community

Steven Straatemans
Steven Straatemans

Posted on

Calculate HTML Element width before render

I want to show you a small trick to know the size of an element, before rendering that element on the screen.
This trick can be useful for various reasons. Developers had to use this trick a lot more in the old days, when we didn't have things like flex and CSS grid and elements couldn't size themselves the way we want them too.
Sometimes you had to calculate the size of your element and set the width by hand.

I hadn't used this trick in ages. But I ran across a small story in a recent project which made me think of this. I had tried all other CSS tricks. I asked my colleagues, but no working solution was suggested.
And after much frustration I thought of this option.

So what was the problem?

I had to build a small component which showed 2 lines of text. So that when the user clicks the "Read more" button it will expand and show all the text.

That doesn't sound complicated at all, does it? Just show X amount of words or Y amount of characters. What are you complaining about?
Well the designer wanted to show 2 lines of text. and at the end of line 2, show the "Read more" button. Like this:
Output of my read-more component

We thought it was a minor task, and I think we did not even bother to poker it. Just a simple chore in a larger story.

And if we would have put the button on the next line, I wouldn't be writing this article. I would just check some line-height and set the overflow of your text element to hidden and be done with it.
But the button really, REALLY had to be at the end of that second line. Designers, right? Right?
You can't fix that with CSS. I first thought of using float: right;
But I would still need to know where to put the element to float. Adding it to the end of the text, would hide the button element.

Somehow, we needed to find a way to know how many words we can fit on that line and also have enough space to accomodate the button.

Ok, so what is the solution?

The easiest way to find out how many words we can fit on those two lines is to throw in one word at a time to see if it fits. And once we go over the two lines, we stop. Easy.

We create a temporary element and append it to the element which will contain the actual text. Because it is a child it will inherit all the styles from our original text element, so all the text will have the correct font-size and line-height etc.
We fill that element word by word and see if the words fit on our two lines ( + our button). And when we go over the two lines, we stop. Once we have the correct amount of text we can remove our temporary element.

The process of the function, finding the amount of visible text

Now that we have the correct amount of text that can fit, we copy that part of the text to the original text element that is visible on your screen. And it will have your button behind it, just the way we planned it.

Our function will look something like this:

const createMaxLines = () => {
  // create the temporary Element
  const ruler = document.createElement('div');
  ruler.style.width = 'auto';
  ruler.style.position = 'absolute';
  ruler.style.whiteSpace = 'nowrap';

  // Adding the element as a child to myElement.
  // it will be added to the DOM
  myElement.appendChild(ruler);

   /**
    * Do the calculations you need to do 
    */

  // clean up after yourself
  myElement.removeChild(ruler);
};
Enter fullscreen mode Exit fullscreen mode

Will that not cause some weird flickering on your screen?

You would think so. We are creating an element. We are adding it to the DOM. It's why I made the temporary element invisible(with CSS) in my first version.
But... The whole function, that checks what text should be visible on our screen, is synchronous. And there are a couple of things that are happening.


But before I can explain that, we first need to look at the process of the render engine in the browser.
There are a couple of steps that need to be taken before an element is shown on your screen.
I will not go into complete detail here, it's to big of a subject, but if you want to learn more in depth about the rendering process, you definitely need to read this article from Tali Garsiel and Paul Irish. It's an oldy, but still awesome.

Alt Text

So first the DOM tree is created, containing a tree with all our HTML tags. Also the CSS is parsed in such a tree.
These two are combined in the render tree, where styles and elements are combined.
The next step is the layout or reflow, where all elements will receive their position.
And finally the paint stage, where the elements will appear on the screen.
Now every time an element is added to the DOM, like in our function, the position of all elements needs to be recalculated in the layout/reflow stage. When that stage is done the screen will be repainted.
Like I said read the article mentioned above for details, what I described here was a gross over-simplification.


As soon as our temporary element is added to the DOM it will trigger a reflow of the render engine.
Now, every time a word is added to the element another reflow is triggered. BUT...not a repaint. The repaint will occur at the end of our function when every calculation is done. And this is the important part, because it is the repaint that will make everything appear on you screen. But at the end of our function, we will remove the temporary element from our DOM, again causing a reflow. Only after that reflow, will the paint part of the render engine run. And because our temporary element is not in the DOM anymore, it will not appear on our screen.

How about performance?

You shouldn't try it with the whole content of "War and Peace", but this option is usually done with just a couple of lines of text and that should be fine.
You can probably improve the performance somewhat, by using a better algorithm to determine how many words will fit.

Conclusion

This is a nice little trick if you need to calculate the size of your element before it will show on your screen.
You will not need it much, because most scenarios nowadays, you can solve with CSS. But for those rare occasions CSS can't help you out, this might do the trick.
Would love to hear from you when you used it in one of your own projects.

I created a small react-component for it, so if you're curious you can find the code here and example here

Top comments (14)

Collapse
 
ismailisimba profile image
Ismaili Simba

Ok, so hear me out, I swear I'm not crazy!

If you know the font size of the text in those two lines, in pixels, this might work...

I assume you get a string like a short description to display there, but at the end of two lines you have to truncate it and display the read more button.

You can calculate the width of the containing element using getComputedStyles then read it as pixels.

Divide that by your font size and you would know the maximum amount of characters you can fit in one line in that container. Multiply it by two and you have your two lines length.

Calculate the width of read more and remove it from the two lines length.

Now all you have to do is truncate the string you are receiving for those two lines at that length.

If you position:relative the container of the two lines. You can position the read more wherever you want using absolute values.

I saw where you said what the easiest way to do this was and this popped up in my mind.

Just wanted to share before I forgot, still reading...

Collapse
 
zachhaber profile image
ZachHaber

I've tried that sort of process before in an application. The one with trying to use line height and the length of the line.
The problem is that it only works properly with monospace fonts. And as I've learned, the users rather dislike reading monospace fonts.

Collapse
 
ismailisimba profile image
Ismaili Simba

Not line height. Just length. You never check line height at all.

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇

Ok hear me out both of you. JavaScript will be executed async after the painting so unless you are using react, svelte, angular or similar it may cause flickering. That being said I used similar trick on the past but with other purposes rather than adding a read more button in which case you need to add visibility: hidden, then to avoid flickering just add a min-height to the parent container and then give priority to this script execution

Thread Thread
 
grahamthedev profile image
GrahamTheDev • Edited

Ok so hear me out all of you 😋.

May I humbly present a solution that is CSS based and only uses JS for the expand, button name change and adding the expanded class.

At less than 50 lines of CSS (and it could be much less) and 20 lines of JavaScript (including the JS to handle the input change updating the CSS variable!) I think it does the job quite nicely.

One downside is it doesn't know if all the lines are shown on screen by default and so can only be used where you know that there will be lines hidden by your height limitation.

Thread Thread
 
sstraatemans profile image
Steven Straatemans

Oeh I like this one, thanks. But yeah, not knowing if all is shown, is too bad.

Thread Thread
 
grahamthedev profile image
GrahamTheDev • Edited

Ok if you insist....😋

Now responds to screen resize and automatically initiates and destroys itself depending on whether it is needed or not.

Resize the below example to mobile size and you should see it all fire into action, resize again and it will automatically remove itself. Also fixed a bug where if you set the clamp to 200 lines it would make the box 200 lines tall! Obviously you can also just edit the number of lines using the input to see it fire into action.

Oh and you could use this with a very large block of text so that is an added bonus (as there is no counting involved).

Perhaps you could take the principle and turn it into a fully fledged component that accounts for incorrect inputs, auto inserts the HTML etc etc.

By the way your example one seems to have an issue if you resize the screen and then change the line values, as you can see in the following example I managed to get it in an open state (showing "read less") while only showing 9 lines when I set it to 10. I am sure mine has issues too but yours is in production so thought I better give you a heads up!

strange issue when resizing the screen

Thread Thread
 
sstraatemans profile image
Steven Straatemans • Edited

@joelbonetr : Thanks for the reply, but about the flickering:
It will not cause flickering, because the actual repaint is done at the end of the function. The reflow is done everytime, but that will not show up on your screen.
I made a small Vanilla JS example:
codesandbox.io/s/calculate-width-j...

Thread Thread
 
sstraatemans profile image
Steven Straatemans

@inhuofficial : niiiice! and thanks for the heads up. I will look into it ASAP

Collapse
 
sstraatemans profile image
Steven Straatemans

the font styles are inherited. so that should work fine with normal fonts as well

Collapse
 
szalonna profile image
Joe

The font size is not the same as font width. If you use non-mono font, you cannot just assume, that the count of letters is in 1-to-1 ratio with the width of the container. Imagine the folloeing two word with the same char coun: wwwwww and llllll.

Collapse
 
sstraatemans profile image
Steven Straatemans

it inherits the style, so also the fonts. So it should be fine

Collapse
 
ismailisimba profile image
Ismaili Simba

Ooh, so that's why it won't work unless you're using monospaced fonts

Collapse
 
sstraatemans profile image
Steven Straatemans

Looks like there is a CSS solution after all.
It uses shape-outside and it has pretty good browser support
The only caveat is that you need to know the height of the element you want in the lower-right corner.
There is a great article about it on CSS-tricks