Immersion in CSS: font metrics, line-height and vertical-align
- Transfer
line-height
and 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-height
you 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 value
font-size
, but the problem is that it font-size: 100px
looks different for different headsets. In this regard, the question arises: line-height
will it always be the same or can it differ? Is this value in the range of 1 to 1.2? And how vertical-align
does 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
p
containing 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-size
in different headsets, the height is different: Even if we know about this feature, why
font-size: 100px
doesn’t it create elements 100px high? I measured these values: Helvetica - 115px, Gruppo - 97px and Catamaran - 164px. 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.
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). Before diving deeper, consider the main points that you will encounter. An element
p
when 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
.
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-line
does 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.
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. 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 value
line-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
,svg
etc...); inline-block
and all type elementsinline-*
;- 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
, margin
and border
. If height
value is specified for auto
, then it is applied line-height
, and the height of the content area is equal line-height
. 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: normal
will 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: 100px
the content area being 112px (1117 units), and the value of line-height: norma
l 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: 1
inefficient. 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. 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-height
greater than 1. In general, their calculated indicator line-height
varies 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-top
andmargin-bottom
there is no effect. - For replaceable inline elements, type elements,
inline-block
and locked inline elements -padding
,margin
andborder
increase,height
and 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-align
can 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
p
with two siblings span
, heritage font-family
, font-size
and fixed line-height
. The baselines are the same, and the height of the row container is equal to them line-height
. But what if the second element
font-size
has 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.
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
p
with line-height: 200px
that contains one single one span
that 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
p
has its own different value font-family
(the default is serif). The baselines of the tag p
and 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.
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 middle
you can not rely on alignment by either. And worst of all, the fact that in most scenarios is middle
never 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.
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-height
can give a strange, but expected result. Finally, it
vertical-align
also 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-height
and 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-size
at 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);
}
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-align
based on the ratio between the upper and lower extension elements. First, we calculate the
line-height: normal
height 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-align
how 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-height
and calculate it, while maintaining vertical alignment:p {
…
/* необходимая высота строки */
--line-height: 3;
line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
}
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;
}
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: n
virtual area, the content area may become smaller. - on
vertical-align
is not particularly rely. - the height of the row container is calculated using properties
line-height
andvertical-align
its children. - We cannot just get or set font metrics through CSS.
But I still love CSS :)
useful links
- Font Metrics: FontForge , opentype.js
- calculate line-height: normal and some proportions in the browser ;
- Ahem is a special font to understand how it works.
- deeper formal explanation of string formatting context
- future spec for vertical alignment help: Line Grid module
- font metrics, API Level 1 , collection of interesting ideas (Houdini)