15

For some fonts when the line-height of the element is smaller than a threshold the scrollHeight is bigger than the clientHeight.

So there is something in the font properties that causes this but what is this and how can it be avoided preferably using CSS or even a font editor for custom fonts?

For example, in this snippet the scrollHeight for Tahoma is more than the clientHeight although sans-serif seems OK when line-height is 1. This difference increases when the page is zoomed in Chrome (ctrl+) and happens even for sans-serif. When the line-height is below 1 or the font-size gets bigger the difference increases.

For some other fonts it goes up to 5px at line-height 1 and decreases by increasing the line-height up to 2 which leads to incorrect calculation.

var a = document.getElementById('a');
console.log('tahoma - a.clientHeight: ' + a.clientHeight);
console.log('tahoma - a.scrollHeight: ' + a.scrollHeight);

var b = document.getElementById('b');
console.log('sans - b.clientHeight: ' + b.clientHeight);
console.log('sans - b.scrollHeight: ' + b.scrollHeight);

var c = document.getElementById('c');
console.log('sans - lineHeight:0.5 - c.clientHeight: ' + c.clientHeight);
console.log('sans - lineHeight:0.5 - c.scrollHeight: ' + c.scrollHeight);

var d = document.getElementById('d');
console.log('sans - font-size:200px - d.clientHeight: ' + d.clientHeight);
console.log('sans - font-size:200px - d.scrollHeight: ' + d.scrollHeight);
div[id] {
 overflow:auto;
 max-width:80%;
}
<div id='a' style='font-family:tahoma; line-height:1;'>Hello</div>
<div id='b' style='font-family:sans-serif; line-height:1;'>Hello</div>
<div id='c' style='font-family:sans-serif; line-height:0.5;'>Hello</div>
<div id='d' style='font-family:sans-serif; line-height: 1; font-size:500px;'>Hello</div>

It's clear that this difference is due to an overflow issue but what font metrics are involved here and how can we identify the difference between scrollHeight and clientHeight?

This happens both in Chrome and Firefox; I didn't test other browsers.

Boann
  • 48,794
  • 16
  • 117
  • 146
Ali
  • 21,572
  • 15
  • 83
  • 95
  • add `overflow: auto;` and you will notice this visually .. this is font specific, each font has its own design and using line-height:1 for tahoma create an overflow, the space needed to draw the font is not enough. – Temani Afif Oct 15 '18 at 11:32
  • add some descender letter and you will better see the issue : https://jsfiddle.net/krcjpdn8/ – Temani Afif Oct 15 '18 at 11:34
  • @TemaniAfif I added overflow:auto to div#c, still the same. I know it's font specific, I want to minimize this effect preferably using css or even a font editor . It cause wrong calculation for expanding elements. – Ali Oct 15 '18 at 11:36
  • To avoid (and/or minimize) this issue, use the default value `1.2` – Asons Oct 15 '18 at 11:36
  • @Ali I know it's still the same ;) overflow will make the issue more visible as you will have a scroll bar with the first one and not the second one ... and it's by design, the Tahoma is overflowing, 1 is not enough as line-height to draw descender letter [as you can see in the code I shared ] – Temani Afif Oct 15 '18 at 11:37
  • @LGSon for some fonts I have to go up to 2, which creates a lot of bad looking empty vertical space. – Ali Oct 15 '18 at 11:37
  • @TemaniAfif I want to know which aspects of font properties cause this, so I can customize my fonts in a font editor to minimize this effect. – Ali Oct 15 '18 at 11:39
  • Yes, I get that...still, not much more you can do, dynamically, since there is no proper method (with script) how to calculate it. – Asons Oct 15 '18 at 11:39
  • I don't know the correct *words* to define this but it's inside the fonts and how they are designed .. basically you choose the space that glyph should take and this will define a lot of metrics specific to this font ... You will probably need a font expert that can tell you exactly what to do – Temani Afif Oct 15 '18 at 11:42
  • @LGSon there is an opentype.js library which can read font-properties and calculate this difference, but I don't know which aspects of font cause this. – Ali Oct 15 '18 at 11:42
  • 1
    check this it may help you : https://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font / https://stackoverflow.com/questions/42026239/what-does-font-size-really-correspond-to / https://stackoverflow.com/questions/25520410/when-setting-a-font-size-in-css-what-is-the-real-height-of-the-letters – Temani Afif Oct 15 '18 at 11:43
  • @TemaniAfif I added font metrics to the question, I know these metrics but I can't figure out which one of them cause this miscalculation. – Ali Oct 15 '18 at 11:48
  • @Ali Add it as a reference in a comment...didn't know there were one. – Asons Oct 15 '18 at 11:49
  • 1
    @LGSon https://opentype.js.org/ it can be used both in node and browser. – Ali Oct 15 '18 at 11:49
  • This is probably out of the programming scope .. I guess you should also post this question in the design stack site .. you will probably get more help there. – Temani Afif Oct 15 '18 at 11:50
  • @TemaniAfif what makes me wonder here is how chrome zoom affects these numbers? – Ali Oct 15 '18 at 12:00
  • 1
    I have to ask, as interesting as it is, why do you care? The bottom line is that you know which fonts overflow by calculating `scrollHeight-clientHeight`, this should be enough to identify the problem and fix it with JS. CSS don't have access to the inner structure of the font, so you won't be able to do such calculation with CSS alone. If you want to know how to alter fonts, I guess stackoverflow is not the preferable platform. You may try : https://graphicdesign.stackexchange.com/ – Itay Gal Oct 15 '18 at 17:28
  • 1
    What do you actually want to be able to do with this knowledge? As you wrote in another comment _"It cause wrong calculation for expanding elements"_, I start thinking you have an X/Y problem here, so instead explain/show what you are trying to achieve and how this discrepancy breaks it. – Asons Oct 15 '18 at 18:32

2 Answers2

3

To make the issue more visible let's introduce a span inside the div and add some border/background. Let's start by using a big line-height:

body {
 font-family:sans-serif;
}
div {
  border:1px solid;
  margin:10px;
}
span {
  background:red;
}
<div style='line-height:3;'><span>Hello</span></div>

The red part define the content area and the space surrounded by the border is the line box which is the height of our div element (check this more information: Why is there space between line boxes, not due to half leading?).

In this case, we don't have any overflow so both scrollHeight and clientHeight will give the same value:

var a = document.getElementById('a');
console.log('clientHeight: ' + a.clientHeight);
console.log('scrollHeight: ' + a.scrollHeight);
body {
 font-family:sans-serif;
}
div {
  border:1px solid;
  margin:10px;
}
span {
  background:red;
}
<div id="a" style='line-height:3;'><span>Hello</span></div>

We can also conclude that both are exacly equal to 3 * 16px which is line-height * font-sizeref (by default the font-size is 16px).

Now if we start deacring the line-height we will logically decrease the height of the div and the content area will remain the same:

body {
 font-family:sans-serif;
}
div {
  border:1px solid;
  margin:10px;
}
span {
  background:red;
}
<div style='line-height:3;'><span>Hello</span></div>
<div style='line-height:1;'><span>Hello</span></div>
<div style='line-height:0.5;'><span>Hello</span></div>
<div style='line-height:0.2;'><span>Hello</span></div>

Now it's clear that we have overflow and the clientHeight will now be less than the scrollHeight but the clientHeight will remain line-height * font-size while the scrollHeight will be the height of the red part:

var a = document.querySelectorAll('.show');
var b = document.querySelectorAll('.show span');

for(var i=0;i<a.length;i++) {
console.log('cH: ' + a[i].clientHeight + ' sH: ' + a[i].scrollHeight);
}
body {
 font-family:sans-serif;
 font-size:100px;
 padding-bottom:100px;
}
div.show {
  border:1px solid;
  margin:100px;
}
span {
  background:red;
}
<div class="show" style='line-height:3;'><span>Hello</span></div>
<div class="show" style='line-height:1;'><span>Hello</span></div>
<div class="show" style='line-height:0.5;'><span>Hello</span></div>
<div class="show" style='line-height:0.2;'><span>Hello</span></div>
<div class="show" style='line-height:0.1;'><span>Hello</span></div>
<div class="show" style='line-height:0;'><span>Hello</span></div>

But why the value of scrollHeight is decreasing while the content area is kept the same? This is due to the fact that we are having an overflow on the top and the bottom (because the alignment is baseline) and the scrollHeight include only the bottom overflow as the top become inaccessible. To make the scrollHeight equal to the content area we simply need to change the alignment:

var a = document.querySelectorAll('.show');
var b = document.querySelectorAll('.show span');

for(var i=0;i<a.length;i++) {
console.log('cH: ' + a[i].clientHeight + ' sH: ' + a[i].scrollHeight);
}
body {
 font-family:sans-serif;
 font-size:100px;
 padding-bottom:100px;
}
div.show {
  border:1px solid;
  margin:100px;
}
span {
  background:red;
  vertical-align:text-bottom;
}
<div class="show" style='line-height:3;'><span>Hello</span></div>
<div class="show" style='line-height:1;'><span>Hello</span></div>
<div class="show" style='line-height:0.5;'><span>Hello</span></div>
<div class="show" style='line-height:0.2;'><span>Hello</span></div>
<div class="show" style='line-height:0.1;'><span>Hello</span></div>
<div class="show" style='line-height:0;'><span>Hello</span></div>

Now it's clear that if the line-height is big enough both are equal and if the line-height is reduced the scrollHeight has a min value equal to the content area.

If we check the specification we can read this:

The 'height' property does not apply. The height of the content area should be based on the font, but this specification does not specify how. A UA may, e.g., use the em-box or the maximum ascender and descender of the font. (The latter would ensure that glyphs with parts above or below the em-box still fall within the content area, but leads to differently sized boxes for different fonts; the former would ensure authors can control background styling relative to the 'line-height', but leads to glyphs painting outside their content area.)

Note: level 3 of CSS will probably include a property to select which measure of the font is used for the content height.

So we cannot know the exact metrics used to define this area, that's why it behave differently for each font. We can only know that it depends on the font-family and the font-size. We may probably find the calculation manually doing some tests. For the above example, the height of the content area seems to be 1.12 * font-size

For tahoma it seems to be 1.206 * font-size (on Chrome) and 1.21 * font-size (on Firefox) (see below):

var a = document.querySelectorAll('.show');
var b = document.querySelectorAll('.show span');

for(var i=0;i<a.length;i++) {
console.log('cH: ' + a[i].clientHeight + ' sH: ' + a[i].scrollHeight);
}
body {
 font-family:tahoma;
 font-size:1000px;
 padding-bottom:100px;
}
div.show {
  border:1px solid;
  margin:100px;
}
span {
  background:red;
  vertical-align:text-bottom;
}
<div class="show" style='line-height:3;'><span>Hello</span></div>
<div class="show" style='line-height:1;'><span>Hello</span></div>
<div class="show" style='line-height:0.5;'><span>Hello</span></div>
<div class="show" style='line-height:0.2;'><span>Hello</span></div>
<div class="show" style='line-height:0.1;'><span>Hello</span></div>
<div class="show" style='line-height:0;'><span>Hello</span></div>

So scrollHeight is equal to p * font-size where p depends on the font and we can find it manually doing some tests and clientHeight is equal to line-height * font-size. Of course, if we keep the alignment baseline, scrollHeight will be different because of the top overflow.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
2

I found that scrollHeight is a measurement of the height of an element's content, including content not visible on the screen due to overflow, while the clientHeight is a measurement of the height of an element.

When you reduce the line-height your div element's height is getting smaller - so the clientHeight will be smaller but the content's height won't be changed, hence the scrollHeight will remain the same, so this is the reason your 2 measurements differs.

If you want 2 different measurements to give the same results, you'll have to modify the container element's height. For example add to the div min-height: 1.2em

var a = document.getElementById('a');
console.log('tahoma - a.clientHeight: ' + a.clientHeight);
console.log('tahoma - a.scrollHeight: ' + a.scrollHeight);

var c = document.getElementById('c');
console.log('sans - lineHeight:0.5 - c.clientHeight: ' + c.clientHeight);
console.log('sans - lineHeight:0.5 - c.scrollHeight: ' + c.scrollHeight);
.div {
  font-size: 40px;
  margin: 10px;
  border: 1px solid black;
  min-height: 1.2em;
}
<div class='div' id='a' style='font-family:tahoma; line-height:1;'>Hello</div>
<div class='div' id='c' style='font-family:sans-serif; line-height:0.5;'>Hello</div>

Obviously, this changes the layout. Without changing the layout you won't be able to measure 2 different things and expect to get the same result.

If you want to calculate the real char size - you can use this solution

console.log("tahoma size: " + measureTextHeight("40px tahoma"));
console.log("sans-serif size: " + measureTextHeight("40px sans-serif"));
console.log("Bookman Old Style size: " + measureTextHeight("40px Bookman Old Style"));
console.log("Palatino Linotype size: " + measureTextHeight("40px Palatino Linotype"));


function measureTextHeight(fontSizeFace) {

    // create a temp canvas
    var width=1000;
    var height=60;
    var canvas=document.createElement("canvas");
    canvas.width=width;
    canvas.height=height;
    var ctx=canvas.getContext("2d");

    // Draw the entire a-z/A-Z alphabet in the canvas
    var text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    ctx.save();
    ctx.font=fontSizeFace;
    ctx.clearRect(0,0,width,height);
    ctx.fillText(text, 0, 40);
    ctx.restore();

    // Get the pixel data from the canvas
    var data = ctx.getImageData(0,0,width,height).data,
        first = false, 
        last = false,
        r = height,
        c = 0;

    // Find the last line with a non-transparent pixel
    while(!last && r) {
        r--;
        for(c = 0; c < width; c++) {
            if(data[r * width * 4 + c * 4 + 3]) {
                last = r;
                break;
            }
        }
    }

    // Find the first line with a non-transparent pixel
    while(r) {
        r--;
        for(c = 0; c < width; c++) {
            if(data[r * width * 4 + c * 4 + 3]) {
                first = r;
                break;
            }
        }

        // If we've got it then return the height
        if(first != r) return last - first;
    }

    // error condition if we get here
    return 0;
}
div{
  font-size: 40px;
}
<div style="font-family:tahoma;">Hello</div>
<div style="font-family:sans-serif;">Hello</div>
<div style="font-family:Bookman Old Style;">Hello</div>
<div style="font-family:Palatino Linotype;">Hello</div>
Itay Gal
  • 10,706
  • 6
  • 36
  • 75
  • I have fonts that difference goes up to 4px not just 1px, but it can be the case for zooming difference. even here, why this happens for tahoma but not sans? – Ali Oct 15 '18 at 12:04
  • for some fonts I have go up to line-height 1.8 to get correct results. – Ali Oct 15 '18 at 12:11
  • it's not about rounding here .. let's forget the JS in all this, adding `overflow:auto` shows clearly the issue which an overflow issue. And this will make the scrollHeight logically different from clientHeight. – Temani Afif Oct 15 '18 at 13:04
  • that proves you are wrong :) increasing the line-height will increase the height of the line box which will logically increase the space for the font and we won't have any overflow ... keep deacresing the line-height and see the resuly by yourself – Temani Afif Oct 15 '18 at 13:19
  • check this : https://jsfiddle.net/4tj6zbqc/ .. I used big values to proove there is no rouding, see the console and you will notice the big difference – Temani Afif Oct 15 '18 at 13:23
  • we already know this and the asker is aware about this [also discussed in the comments] and the question is "how to avoid it" ... it's clear that both are different so saying this will not add anything IMHO – Temani Afif Oct 15 '18 at 13:27
  • @TemaniAfif exactly, the problem arise when element is expanded using height and height is set to auto at the end of transition for further expansion of it's interior elements. There must be some ways to calculate this difference and subtract it from scrollHeight before expansion. – Ali Oct 15 '18 at 13:41
  • @Ali you only presented a general problem, but didn't present what you actually need, so it's hard to suggest a workaround for your specific problem – Itay Gal Oct 15 '18 at 13:43
  • @ItayGal I asked what aspect of fonts affects this difference? – Ali Oct 15 '18 at 13:45
  • what you calculated in your second code is still not relevant .. check this : https://stackoverflow.com/questions/25520410/when-setting-a-font-size-in-css-what-is-the-real-height-of-the-letters .. you simply cacluated the body height which as you can notice lower than the font-size and this height is not useful to identify the issue as the height we need is bigger than the font size. The height we need include more space above and below the character that's why we have overflow – Temani Afif Oct 15 '18 at 15:56