Line-height
and vertical-align
are simple CSS properties. So simple that most of us are convinced to fully understand how they work and how to use them. But it’s not. They really are complex, maybe the hardest ones, as they have a major role in the creation of one of the less-known feature of CSS: inline formatting context.
For example, line-height
can be set as a length or a unitless value 1, but the default is normal
. OK, but what normal is? We often read that it is (or should be) 1, or maybe 1.2, even the CSS spec is unclear on that point. We know that unitless line-height
is font-size
relative, but the problem is that font-size: 100px
behaves differently across font-families, so is line-height
always the same or different? Is it really between 1 and 1.2? And vertical-align
, what are its implications regarding line-height
?
Deep dive into a not-so-simple CSS mechanism…
Let’s talk about font-size
first
Look at this simple HTML code, a <p>
tag containing 3 <span>
, each with a different font-family
:
<p>
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
</p>
p { font-size: 100px }
.a { font-family: Helvetica }
.b { font-family: Gruppo }
.c { font-family: Catamaran }
Using the same font-size
with different font-families produce elements with various heights:
Even if we’re aware of that behavior, why font-size: 100px
does not create elements with 100px height? I’ve measured and found these values: Helvetica: 115px, Gruppo: 97px and Catamaran: 164px
Although it seems a bit weird at first, it’s totally expected. The reason lays down inside the font itself. Here is how it works:
- a font defines its em-square (or UPM, units per em), a kind of container where each character will be drawn. This square uses relative units and is generally set at 1000 units. But it can also be 1024, 2048 or anything else.
- based on its relative units, metrics of the fonts are set (ascender, descender, capital height, x-height, etc.). Note that some values can bleed outside of the em-square.
- in the browser, relative units are scaled to fit the desired font-size.
Let’s take the Catamaran font and open it in FontForge to get metrics:
- the em-square is 1000
- the ascender is 1100 and the descender is 540. After running some tests, it seems that browsers use the HHead Ascent/Descent values on Mac OS, and Win Ascent/Descent values on Windows (these values may differ!). We also note that Capital Height is 680 and X height is 485.
That means the Catamaran font uses 1100 + 540 units in a 1000 units em-square, which gives a height of 164px when setting font-size: 100px
. This computed height defines the content-area of an element and I will refer to this terminology for the rest of the article. You can think of the content-area as the area where the background
property applies 2.
We can also predict that capital letters are 68px high (680 units) and lower case letters (x-height) are 49px high (485 units). As a result, 1ex
= 49px and 1em
= 100px, not 164px (thankfully, em
is based on font-size
, not computed height)
Before going deeper, a short note on what this it involves. When a <p>
element is rendered on screen, it can be composed of many lines, according to its width. Each line is made up of one or many inline elements (HTML tags or anonymous inline elements for text content) and is called a line-box. The height of a line-box is based on its children’s height. The browser therefore computes the height for each inline elements, and thus the height of the line-box (from its child’s highest point to its child’s lowest point). As a result, a line-box is always tall enough to contain all its children (by default).
Each HTML element is actually a stack of line-boxes. If you know the height of each line-box, you know the height of an element.
If we update the previous HTML code like this:
<p>
Good design will be better.
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
We get to make a consequence.
</p>
It will generate 3 line-boxes:
- the first and last one each contain a single anonymous inline element (text content)
- the second one contains two anonymous inline elements, and the 3
<span>
We clearly see that the second line-box is taller than the others, due to the computed content-area of its children, and more specifically, the one using the Catamaran font.
The difficult part in the line-box creation is that we can’t really see, nor control it with CSS. Even applying a background to ::first-line
does not give us any visual clue on the first line-box’s height.
line-height
: to the problems and beyond
Until now, I introduced two notions: content-area and line-box. If you’ve read it well, I told that a line-box’s height is computed according to its children’s height, I didn’t say its children content-area’s height. And that makes a big difference.
Even though it may sound strange, an inline element has two different height: the content-area height and the virtual-area height (I invented the term virtual-area as the height is invisible to us, but you won’t find any occurrence in the spec).
- the content-area height is defined by the font metrics (as seen before)
- the virtual-area height is the
line-height
, and it is the height used to compute the line-box’s height
That being said, it breaks down the popular belief that line-height
is the distance between baselines. In CSS, it is not 3.
The computed difference of height between the virtual-area and the content-area is called the leading. Half this leading is added on top of the content-area, the other half is added on the bottom. The content-area is therefore always on the middle of the virtual-area.
Based on its computed value, the line-height
(virtual-area) can be equal, taller or smaller than the content-area. In case of a smaller virtual-area, leading is negative and a line-box is visually smaller than its children.
There are also other kind of inline elements:
- replaced inline elements (
<img>
,<input>
,<svg>
, etc.) inline-block
and allinline-*
elements- inline elements that participate in a specific formatting context (eg. in a flexbox element, all flex items are blocksified)
For these specific inline elements, height is computed based on their height
, margin
and border
properties. If height
is auto
, then line-height
is used and the content-area is strictly equal to the line-height
.
Anyway, the problem we’re still facing is how much the line-height
’s normal
value is? And the answer, as for the computation of the content-area’s height, is to be found inside the font metrics.
So let’s go back to FontForge. The Catamaran’s em-square is 1000, but we’re seeing many ascender/descender values:
- generals Ascent/Descent: ascender is 770 and descender is 230. Used for character drawings. (table “OS/2”)
- metrics Ascent/Descent: ascender is 1100 and descender is 540. Used for content-area’s height. (table “hhea” and table “OS/2”)
- metric Line Gap. Used for
line-height: normal
, by adding this value to Ascent/Descent metrics. (table “hhea”)
In our case, the Catamaran font defines a 0 unit line gap, so line-height: normal
will be equal to the content-area, which is 1640 units, or 1.64.
As a comparison, the Arial font describes an em-square of 2048 units, an ascender of 1854, a descender of 434 and a line gap of 67. It means that font-size: 100px
gives a content-area of 112px (1117 units) and a line-height: normal
of 115px (1150 units or 1.15). All these metrics are font-specific, and set by the font designer.
It becomes obvious that setting line-height: 1
is a bad practice. I remind you that unitless values are font-size
relative, not content-area relative, and dealing with a virtual-area smaller than the content-area is the origin of many of our problems.
But not only line-height: 1
. For what it’s worth, on the 1117 fonts installed on my computer (yes, I installed all fonts from Google Web Fonts), 1059 fonts, around 95%, have a computed line-height
greater than 1. Their computed line-height
goes from 0.618 to 3.378. You’ve read it well, 3.378!
Small details on line-box computation:
- for inline elements,
padding
andborder
increases the background area, but not the content-area’s height (nor the line-box’s height). The content-area is therefore not always what you see on screen.margin-top
andmargin-bottom
have no effect. - for replaced inline elements,
inline-block
and blocksified inline elements:padding
,margin
andborder
increases theheight
, so the content-area and line-box’s height
vertical-align
: one property to rule them all
I didn’t mention the vertical-align
property yet, even though it is an essential factor to compute a line-box’s height. We can even say that vertical-align
may have the leading role in inline formatting context.
The default value is baseline
. Do you remind font metrics ascender and descender? These values determine where the baseline stands, and so the ratio. As the ratio between ascenders and descenders is rarely 50/50, it may produce unexpected results, for example with siblings elements.
Start with that code:
<p>
<span>Ba</span>
<span>Ba</span>
</p>
p {
font-family: Catamaran;
font-size: 100px;
line-height: 200px;
}
A <p>
tag with 2 siblings <span>
inheriting font-family
, font-size
and fixed line-height
. Baselines will match and the line-box’s height is equal to their line-height
.
What if the second element has a smaller font-size
?
span:last-child {
font-size: 50px;
}
As strange as it sounds, default baseline alignment may result in a higher (!) line-box, as seen in the image below. I remind you that a line-box’s height is computed from its child’s highest point to its child’s lowest point.
That could be an argument in favor of using line-height
unitless values, but sometimes you need fixed ones to create a perfect vertical rhythm. To be honest, no matter what you choose, you’ll always have trouble with inline alignments.
Look at this another example. A <p>
tag with line-height: 200px
, containing a single <span>
inheriting line-height
<p>
<span>Ba</span>
</p>
p {
line-height: 200px;
}
span {
font-family: Catamaran;
font-size: 100px;
}
How high is the line-box? We should expect 200px, but it’s not what we get. The problem here is that the <p>
has its own, different font-family
(default to serif
). Baselines between the <p>
tag and the <span>
are likely to be different, the height of the line-box is therefore higher than expected. This happens because browsers do their computation as if each line-box starts with a zero-width character, that the spec called a strut.
An invisible character, but a visible impact.
To resume, we’re facing the same previous problem as for siblings elements.
Baseline alignment is screwed, but what about vertical-align: middle
to the rescue? As you can read in the spec, middle
“aligns the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent”. Baselines ratio are different, as well as x-height ratio, so middle
alignment isn’t reliable either. Worst, in most scenarios, middle
is never really “at the middle”. Too many factors are involved and cannot be set via CSS (x-height, ascender/descender ratio, etc.)
As a side note, there are 4 other values, that may be useful in some cases:
vertical-align: top
/bottom
align to the top or the bottom of the line-boxvertical-align: text-top
/text-bottom
align to the top or the bottom of the content-area
Be careful though, in all cases, it aligns the virtual-area, so the invisible height. Look at this simple example using vertical-align: top
. Invisible line-height
may produce odd, but unsurprising, results.
Finally, vertical-align
also accepts numerical values which raise or lower the box regarding to the baseline. That last option could come in handy.
CSS is awesome
We’ve talked about how line-height
and vertical-align
work together, but now the question is: are font metrics controllable with CSS? Short answer: no. Even if I really hope so.
Anyway, I think we have to play a bit. Font metrics are constant, so we should be able to do something.
What if, for example, we want a text using the Catamaran font, where the capital height is exactly 100px high? Seems doable: let’s do some maths.
First we set all font metrics as CSS custom properties 4, then compute font-size
to get a capital height of 100px.
p {
/* font metrics */
--font: Catamaran;
--fm-capitalHeight: 0.68;
--fm-descender: 0.54;
--fm-ascender: 1.1;
--fm-linegap: 0;
/* desired font-size for capital height */
--capital-height: 100;
/* apply font-family */
font-family: var(--font);
/* compute font-size to get capital height equal desired font-size */
--computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
font-size: calc(var(--computedFontSize) * 1px);
}
Pretty straightforward, isn’t it? But what if we want the text to be visually at the middle, so that the remaining space is equally distributed on top and bottom of the “B” letter? To achieve that, we have to compute vertical-align
based on ascender/descender ratio.
First, compute line-height: normal
and content-area’s height:
p {
…
--lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));
--contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}
Then, we need:
- the 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 so:
p {
…
--distanceBottom: (var(--fm-descender));
--distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
}
We can now compute vertical-align
, which is the difference between the distances multiplied by the computed font-size
. (we must apply this value to an inline child element)
p {
…
--valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
vertical-align: calc(var(--valign) * -1px);
}
At the end, we set the desired line-height
and compute it while maintaining a vertical alignment:
p {
…
/* desired line-height */
--line-height: 3;
line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
}
Adding an icon whose height is matching the letter “B” is now easy:
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;
}
Note that this test is for demonstration purpose only. You can’t rely on this. Many reasons:
- unless font metrics are constant, computations in browsers are not ¯\(ツ)/¯
- if font is not loaded, fallback font has probably different font metrics, and dealing with multiple values will quickly become quite unmanageable
Takeaways
What we learned:
- inline formatting context is really hard to understand
- all inline elements have 2 height:
- the content-area (based on font metrics)
- the virtual-area (
line-height
) - none of these 2 heights can be visualize with no doubt. (if you're a devtools developer and want to work on this, it could be awesome)
line-height: normal
is based on font metricsline-height: n
may create a virtual-area smaller than content-areavertical-align
is not very reliable- a line-box’s height is computed based on its children’s
line-height
andvertical-align
properties - we cannot easily get/set font metrics with CSS
But I still love CSS :)
Resources
- get font metrics: FontForge, opentype.js
- compute
line-height: normal
, and some ratio in the browser - Ahem, a special font to help understand how it works
- an even deeper, institutional, explanation of inline formatting context
- Capsize, a tool to make the sizing and layout of text predictable
- Up to date specification CSS Inline Layout Module Level 3
- A blog post about the
leading-trim
property, to ensure consistent spacing by controling the leading - Font Metrics API Level 1, a collection of interesting ideas (Houdini)
- no matter what you choose, that’s not the point here ↩
- it’s not strictly true ↩
- in other editing software, it might be the distance between baselines. In Word, or Photoshop, that is the case. The main difference is that the first line is also affected in CSS ↩
- you could also use variables from preprocessors, custom properties are not required ↩