Immersion in CSS: font metrics, line-height and vertical-align

Original author: Vincent De Oliveira
  • Transfer
image

line-heightand vertical-align- these are simple CSS properties. So simple that most of us are sure that we understand how they work and how to use them. Unfortunately, this is not true - in fact, they are perhaps the most complex properties, because they play an important role in the creation of a little-known features of CSS, called " line formatting context » (inline formatting context).

For example, line-heightyou can specify it as a length or a dimensionless value, but its default value is normal(standard). OK, but what does “standard” mean? They often write that this is (as a rule) 1, or maybe 1.2. Even in the CSS specification, there is no clear answer to this question .

We know that the dimensionless line-height value depends on the valuefont-size, but the problem is that it font-size: 100pxlooks different for different headsets. In this regard, the question arises: line-heightwill it always be the same or can it differ? Is this value in the range of 1 to 1.2? And how vertical-aligndoes it affect line-height?

Let's dig into the not-so-simple CSS mechanism ...

Let's start by talking about font-size.


Consider this simple HTML code with a tag pcontaining three elements span, each with its own font-family:

BaBaBa


p  { font-size: 100px }
.a { font-family: Helvetica }
.b { font-family: Gruppo }
.c { font-family: Catamaran }

When using the same thing font-sizein different headsets, the height is different:

image

Even if we know about this feature, why font-size: 100pxdoesn’t it create elements 100px high? I measured these values: Helvetica - 115px, Gruppo - 97px and Catamaran - 164px.

image

Although at first glance it looks somewhat strange, everything is quite expected - the reason is in the font itself . How it works:

  • The font sets its own em-square (em-square) (aka UPM, units per em - units per pin) - a kind of pad within which each character will be drawn. In this square, relative units are used for measurement, and, as a rule, sizes of 1000 units are taken for it. Although it also happens 1024, 2048 or a different number of units.
  • Depending on the number of relative units, font metrics are set, such as the height of the upper and lower extension elements (ascender / descender), upper and lower case letters. Some values ​​may go beyond the em square.
  • In the browser, relative units are scaled to the necessary font-size.

Take the Catamaran font and open it in FontForge to get the metrics:

  • em-square is taken as 1000 units;
  • the height of the upper remote elements is 1100 units, and the lower ones are 540.

After several checks, it turned out that browsers on Mac OS use HHead Ascent / Descent, and on Windows - Win Ascent / Descent (these values ​​may vary). In addition, the height of the capital letters Capital Height is 680 units, and the lowercase X height is 485.

image

Thus, the Catamaran font uses 1100 + 540 units in an em-square consisting of 1000 units, and therefore with a font-size of 100px the height is obtained 164px. This calculated height defines the content-area of ​​the element (this term will be used hereinafter). You can consider the content area as the area to which the property applies background.

You can also assume that the height of the uppercase letters is 68px (680 units), and the lowercase (x-height) is 49px (485 units). As a result, 1ex = 49px and 1em = 100px, not 164px (fortunately, em depends on font-size, not on the calculated height).

image

Before diving deeper, consider the main points that you will encounter. An element pwhen displayed on the screen may consist of several lines with the appropriate width. Each line consists of one or more inline elements (HTML tags or anonymous inline elements for text content) and is called a line-box container. The height of the row container depends on the heights of its children. That is, the browser calculates the height of each line item, and from it - the height of the line container (from the highest to the lowest point of its children). As a result, the height of the container of the string is always enough to accommodate all its children (by default).

Each HTML element is actually a stack of string containers. If you know the height of all containers in a row, then the height of the element is also known.

When changing the above HTML code as follows:

Good design will be better. BaBaBa We get to make a consequence.


three row containers will be generated:

  • in the first and last there will be one anonymous string element (text content);
  • the second will have two anonymous string elements and 3 elements span.

image

It is clearly seen that the second container of the row is larger than the others in height due to the calculated content area of ​​its children, more precisely the one that uses the Catamaran font.

The tricky part about creating a string container is that, in fact, we can neither see nor manipulate it through CSS . Even applying the background to ::first-linedoes not help display the height of the first container of the row.

line-height: about problems and other issues


Up to this point, I have introduced two concepts - the content area and the string container. If you read carefully, you noticed that the height of the row container is calculated based on the height of its children, but did not say that it is based on the height of the content area of ​​its children. And this is a big difference.

Even if this may seem strange, the line element has two different heights: the height of the content area and the height of the virtual area (I myself coined the term “virtual area”, because we cannot see this height; in the specification of this term, you will not find).

  • The height of the content area is determined by the font metrics (as we saw earlier).
  • The height of the virtual-area is line-height, and this is the height that is used to calculate the height of the row container.

image

In addition, the above refutes the widespread belief that line-height- this is the distance between the baselines (baseline). In CSS, this is not the case.

image

In other editorial programs, this may be the distance between the baselines. For example, in Word and Photoshop it is. The main difference is that in CSS this distance is for the first line too.

The calculated height difference between the virtual area and the content area is called leading. One half of the leading is added to the top of the content area, and the second to the bottom. Therefore, the content area is always centered on the virtual area.

Depending on the calculated valueline-height(virtual area) may be equal to, larger or smaller than the content area. If the virtual area is smaller, then the leading value is negative and the row container is visually smaller than its children in height.

There are other types of lowercase elements:

  • lowercase displaceable elements ( img, input, svgetc...);
  • inline-blockand all type elements inline-*;
  • Inline elements that are involved in a particular formatting context (for example, in a flexbox element, all flex components are locked).

For such special line items, the height is calculated based on their properties height, marginand border. If heightvalue is specified for auto, then it is applied line-height, and the height of the content area is equal line-height.

image

And yet the problem remains the same: what is the normal value for line-height? The answer to this question, as in the case of calculating the content area, must be found among the font metrics. So back to FontForge. The em-square for Catamaran is 1000, but we see many values ​​for the top and bottom extension elements:

  • General values ​​of Ascent / Descent: the height of the top detail is 770, the bottom is 230. Used to create characters (table “OS / 2”).
  • Ascent / Descent metrics: the height of the top extension is 1100, the bottom is 540. Used to determine the height of the content area (tables “hhea” and “OS / 2”).
  • Line Gap metric (line spacing). Used to determine line-height: normal, this value is added to the Ascent / Descent metrics (table "hhea").

In our case, the Catamaran font determines that the line spacing is 0 units, and thus line-height: normalwill be equal to the content area, which is 1640 units or 1.64.

As a comparison: for the Arial font, the em-square is 2048 units, the height of the top detail is 1854, the bottom is 434, the line spacing is 67. Thus, with font-size: 100pxthe content area being 112px (1117 units), and the value of line-height: normal is 115px (1150 units or 1.15). All these metrics are individual for each font and are set by the font designer.

Therefore, asking is line-height: 1inefficient. Let me remind you that dimensionless values ​​depend onfont-size, and not from the content area, but the fact that the size of the content area exceeds the size of the virtual area is the cause of many of our problems.

image

But the reason is not only in line-height: 1. For that matter, out of 1,117 fonts installed on my computer (yes, I installed all the fonts from Google Web Fonts), 1,059 fonts, that is, 95%, the calculated indicator is line-heightgreater than 1. In general, their calculated indicator line-heightvaries from 0.618 up to 3.378. (It didn’t seem to you - 3.378!)

Small details regarding the calculation line-box:

  • For inline elements, padding and border increase the background area, but not the height of the content area (and not the height of the line container). Therefore, the content area is not always what is visible on the screen. From margin-topand margin-bottomthere is no effect.
  • For replaceable inline elements, type elements, inline-blockand locked inline elements - padding, marginand borderincrease, heightand therefore, the height of the content area and row container.

vertical-align: the property that controls everything


I have not yet dwelled in detail on the vertical-align property, although it is the main factor for calculating the height of the row container. You could even say that it vertical-aligncan play a leading role in the lowercase formatting context.

Its default value is baseline. Remember font metrics such as the height of the top and bottom detail (ascender / descender)? These values ​​determine where the baseline is and, therefore, the ratio between the upper and lower parts. Since the ratio between the upper and lower extension elements is rarely 50/50, this can lead to unexpected results, for example with elements of the same level.

Let's start with this code:

BaBa


p {
    font-family: Catamaran;
    font-size: 100px;
    line-height: 200px;
}

Tag pwith two siblings span, heritage font-family, font-sizeand fixed line-height. The baselines are the same, and the height of the row container is equal to them line-height.

image

But what if the second element font-sizehas less?

span:last-child {
    font-size: 50px;
}

No matter how strange it may sound, aligning the default baseline can lead to an increase in the height (!) Of the row container, as shown in the figure below. I remind you that the height of the row container is calculated from the highest to the lowest point of its children.

image

This could be an argument in favor of dimensionless values line-height, but sometimes fixed values ​​are required to create an ideal vertical rhythm. Honestly, no matter what you choose, you will always have problems aligning the line.

Consider another example. A tag pwith line-height: 200pxthat contains one single one spanthat inherits itline-height

Ba


p {
    line-height: 200px;
}
span {
    font-family: Catamaran;
    font-size: 100px;
}

What is the height of the row container? We could assume that 200px, but it is not. The problem is that it phas its own different value font-family(the default is serif). The baselines of the tag pand span, in all likelihood, are at different heights, and therefore the height of the line container is greater than expected. This is because browsers perform the calculation, assuming that each container of the string starts with a zero-width character, which is called “strut” in the specification.

Invisible symbol with visible effect.

So, we still have the same problem as in the case of siblings.

image

With alignment on the baseline, everything is bad, but maybe it will save us vertical-align: middle? As stated in the spec,middle"Aligns the container to the vertical midpoint (midpoint) with the baseline of the parent container plus half the x-height of the primary element." The ratio of baselines, as well as x-heights (x-height), can be different, so middleyou can not rely on alignment by either. And worst of all, the fact that in most scenarios is middlenever truly “centered”. This is influenced by too many factors that cannot be set via CSS (x-height, ratio of upper and lower detail elements, etc.).

In addition to this, there are four other values ​​that may prove useful in some cases:

  • vertical-align: top / bottom - alignment on the upper or lower border of the container line;
  • vertical-align: text-top / text-bottom - alignment on the upper or lower border of the content area.

image

But be careful: in all cases, the virtual area is aligned, that is, the invisible height. Consider a simple example using vertical-align: top. Invisible line-heightcan give a strange, but expected result.

image

Finally, it vertical-alignalso accepts numerical values ​​that move the container higher or lower relative to the baseline. This last option may come in handy.

CSS is amazing


We discussed the issue of interaction line-heightand vertical-align, but now the question is, is it possible to control font metrics through CSS? In short, no. Although I would really like that. In any case, I think it's time to have some fun. Font metrics are constant values, so at least something should work out for us.

What if, for example, we need text in Catamaran font with exactly 100px capital letters? It seems feasible, so let's do some calculations.

First of all, we indicate all font metrics as custom CSS properties, and then we calculate font-sizeat which the height of the capital letters will be 100px.

p {
    /* метрики шрифта */
    --font: Catamaran;
    --fm-capitalHeight: 0.68;
    --fm-descender: 0.54;
    --fm-ascender: 1.1;
    --fm-linegap: 0;
    /* необходимый размер шрифта для высоты прописных букв */
    --capital-height: 100;
    /* применить font-family */
    font-family: var(--font);
    /* рассчитать размер шрифта для получения высоты прописных букв, равной необходимому размеру шрифта */
    --computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
    font-size: calc(var(--computedFontSize) * 1px);
}

image

Pretty simple, right? But what if we need the text to be visually centered and the remaining space evenly distributed above and below the letter “B”? To do this, it is necessary to calculate vertical-alignbased on the ratio between the upper and lower extension elements.

First, we calculate the line-height: normalheight of the content area:

p {
    …
    --lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));
    --contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}

Then we need:

  • distance from the bottom of the capital letter to the bottom edge;
  • the distance from the top of the capital letter to the top edge.

Like that:

p {
    …
    --distanceBottom: (var(--fm-descender));
    --distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
}

Now we can calculate vertical-alignhow the difference between these distances, multiplied by the calculated value font-size(This value must be applied to the lowercase child).

p {
    …
    --valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
    vertical-align: calc(var(--valign) * -1px);
}

And finally, we set the necessary value line-heightand calculate it, while maintaining vertical alignment:

p {
    …
    /* необходимая высота строки */
    --line-height: 3;
    line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
}

image

Now it’s quite simple to add a graphic element of the same height as the letter “B”:

span::before {
    content: '';
    display: inline-block;
    width: calc(1px * var(--capital-height));
    height: calc(1px * var(--capital-height));
    margin-right: 10px;
    background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');
    background-size: cover;
}

image

I remind you that this test is shown solely for demonstration, and you should not rely on its results. There are many reasons:

  • If the font metrics are inconsistent, then the calculations in the browser will not be constant. ¯ (ツ) / ¯
  • If the font did not load, then the backup font metrics will most likely be different, and it will become impossible to work with many values ​​quickly.


To summarize


Working examples:


What we found out:
  • The inline formatting context is really hard to understand.
  • All lowercase elements have two heights:
    • height of the content area (which depends on the font metrics);
    • virtual area height ( line-height);
    • None of them can definitely be visualized (unless you are engaged in development tools and decided to fix this defect - then it would be just wonderful).

  • line-height: normal depends on font metrics.
  • Due to the line-height: nvirtual area, the content area may become smaller.
  • on vertical-alignis not particularly rely.
  • the height of the row container is calculated using properties line-heightand vertical-alignits children.
  • We cannot just get or set font metrics through CSS.


But I still love CSS :)

useful links



Also popular now: