line-height
和vertical-align
在CSS中是两个简单的属性。如此简单,大多数人都相信自己已经完全理解它们是如何工作的以及如何使用它们。但事实上并不如此。他们其实很复杂,也是CSS中难点之一,而且也是CSS中特性之一:内联格式化上下文(inline formatting context)。
比如可以设置line-height
带有长度单位的值或一个无单位的值,但其默认值是normal
。那么在CSS中normal
是什么呢?我们常常认为它是(或者应该是)1
或者1.2
,甚至也可以说,CSS规范都不清楚是哪一个。我们也知道,没有单位的line-height
是相对于font-size
的,但问题是,font-size: 100px;
在使用不同的字体(font-family
)表现的行为是不一样的,所以line-height
总是相同或不同的吗?真的是1
还是1.2
吗?另外vertical-align
对line-height
的影响又是什么呢?
要深入研究CSS的机制可以说没有这么简单......
首先来聊font-size
首先来看一个简单的HTML代码,一个<p>
标签中包含了三个<span>
标签,每个<span>
都使用不同的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;
}
每个元素使用相同的font-size
,但使用不同的font-family
,但渲染出来的line-height
是不同的:
即使我们意识到这种行为,但还是不清楚为什么font-size:100px
时元素的height
不是100px
?我测量发现:Helvetica
字体的高度是115px
,Gruppo
字体的高度是97px
和Catamaran
字体的高度是164px
。
起初似乎有点奇怪,但它是完全可预期的。这主要还是font-family
的原因。那就要搞清楚它是如何工作的:
- 字体定义其em-square,每个字符将会绘制出自己的容器。这个正方形使用相对单位和生成一个
1000
单位。但它也可以是1024
,2048
或者其他 - 根据其荐对单位,字体的度量可以根据一些设置(ascender,descender,capital height,x-height等)来决定。注意,有些值是em-square之外的值
- 在浏览器中,相对单位是用于缩放用来适应所需的
font-size
让我们来看Catamaran
字体,并且在FontForge中来看这个字体的度量参数:
- em-square是
1000
- 上升(ascender)是
1100
和下降(descender)是540
。相同的测试下,浏览器使用HHead Ascent/Descent值(Mac)和Win Ascent/Descent值(Windows),这些值可能不同。我们还需要注意,Capital高度是640
和x-height的值是485
这意味着Catamaran
字体在1000
个单位的em-square使用了1100 + 540
个单位,也就是说font-size:100px
的时候,其高度是164px
。这个计算高度定义了元素内容高度(在这篇文章中其它部分引用这个术语content-area)。你可能想到是内容区域相当于background
属性。
我们也可以预测,大写字母是68px
高度(680个单位)和小写字母(x-hegiht)是49px
高度(485个单位)。因此,1ex = 49px
和1em = 100px
,而不是164px
(值得庆幸的是,em
是基于font-size
计算,而不是height
)。
在继续深入之前,先要了解这涉及到什么?当<p>
元素呈现在屏幕上,它根据它的宽度可以有很多线。每一行是由一个或多个行内元素(HTML标签元素或匿名内联元素文本内容)组成,专业术语称为行盒(line-box)。line-box的高度是基于它的子元素高度的。浏览器为每个行内元素计算的高度都是line-box(子元素的最高点到最低点)。因此line-box的总高度足以包含所有子元素(默认情况下)。
每个HTML元素实际上是一个line-box的堆栈。如果你知道每个line-box的高度,实际上你就知道每个元素的高度。
如果我们把前面的HTML结构更新成:
<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>
它会生成三个line-box:
- 第一个和最后一个每个包含一个匿名内联元素(文本内容)
- 第二个包含了两个匿名内联元素和三个
<span>
<p>
元素(黑色边框)产生了一个line-box
(白色边框),其包含了内联元素(实心边框)和匿名内联元素(虚线边框)。
我们清楚的看到,第二个line-box明显比其他的line-box要更高,根据子元素的内容区域(content-area)计算得来,更具体地说,是使用了Catamaran
字体。
困难的是line-box创建部分是我们无法看到的,也不是用CSS控制它。即使在::first-line
应用了background
也无法直接在视觉上看到第个line-box的高度。
line-height问题
直到现在,我们介绍了两个概念:content-area和line-box。如果仔细阅读了前面的内容,你应该知道line-box的高度是根据子元素高度来计算,而且我并没有说是子元素的内容区域(content-area)的高度。这是有很大区别的。
尽管这听起来可能有些奇怪,内联元素有两个不同高度:内容区域(content-area)高度和虚拟区域(virtual-area)高度(这是我发明的术语virtual-area高度,你在规范中是找不到任何相关的内容)。
- 内容区域高度是由字体来决定的(前面介绍过)
- 虚拟区域(virtual-area)高度是
line-height
,它的高度用于计算line-box的高度
行内元素有两个不同的高度。
也就是说,line-height
普遍的看法是不同基线(baseline)的距离。在CSS中,它并不是这样。
计算虚拟区域(virtual-area)和内容区域(content-area)高度差称为leading。leading添加在内容区域顶部,另一半添加在内容区域底部。因此,内容区域总是在虚拟区域的中间。
根据其计算值,line-height
(virtual-area)相同情况下比content-area更高或更低。对于较小的virtual-area,leading是负值和line-box要比它的子元素更小。
还有其他的内联元素:
- 替代内联行内元素(
<img>
,<input>
,<svg>
等) inline-block
元素- 行内元素参与特定格式化上下文(如,Flexbox元素,和所有的Flex项目)
对于这些特定的行内元素,高度计算基于他们的height
、margin
和border
属性。如果hegiht
的值是auto
,然后使用line-height
时content-area严格上等于line-height
。
无论如何,我们仍然面临的问题是line-height
的normal
值是多小?答案是,其计算content-area高度还是依据于里面的字体来度量。
我们回到FontForge。Catamaran的em-square是1000,但我们看到ascender/descender的值:
- 生成的Ascent/Descent: ascender是770,descender是230。用于绘制字符(OS/2)
- 度量的Ascent/Descent: ascender是1100,descender是540。用于内容区域高度(hhea和OS/2)
- 度量线的间距:通过Ascent/Descent度量使用
line-height: normal
(hhea)
在我们的示例中,Catamaran字体定义了0
个单位的线间距(Line Gap),因此line-height: normal
的值将等于内容区域,也就是1640
个单位或1.64
。
作为比较,Arial字体的一个em-square是2048
个单位,其ascender是1854
,descender是434
,线间距是67
。这意味着,font-size: 100px
的内容区域是112px
(1117
个单位)和line-height
是115px
(1150
个单位或1.15
)。所有这些度量都是特殊字型,由字体设计师来设置。
显而易见,设置line-height:1
是一个非常糟糕的做法。我提醒你,font-size
没有单位的观念是相对的,但内容区域不是相对的以及处理虚拟区域小于内容区域有很多问题存在。
但并是只有line-height:1
。不论真假,我电脑上安装了1117
种字体(是的,我安装了所有的Google Web字体),其中1059
种字体,占全部字体的95%
左右,计算的line-height
大于1
。它们计算line-height
是从0.618
到3.378
。你得记住,是3.378
!
line-box计算的小细节:
- 对于内联元素,
padding
和border
增加了其background
区域,但不会增加内容区域高度(甚至是line-box高度)。因此,你在屏幕上看到的不一定就是内容区域。margin-top
和margin-bottom
对内联元素不生效。 - 对于行内替代元素,
inline-block
和blocksified行内元素,padding
,margin
和border
都会增加高度,所以内容区域和line-box的高度也会增加
vertical-align:一个属性控制一切
前面我没有提到vertical-align
属性,即使它是计算line-box高度的一个重要因素。我们甚至可以说,vertical-align
属性对于行内格式化上下文中的leading有很大的作用。
vertical-align
的默认值是baseline
。你注意到度量字体的ascender和descender?这些值是基于baseline
,具有一定的比例。那么ascender和descender之间的比例真的是50/50
,它可能会产生意想不到的结果,例如所有兄弟元素。
先从这个代码开始:
<p>
<span>Ba</span>
<span>Ba</span>
</p>
p {
font-family: Catamaran;
font-size: 100px;
line-height: 200px;
}
两个<span>
元素继承了<p>
元素的font-family
,font-size
和固定的line-height
。基线将会匹配以入line-box的高度等于他们的line-height
。
如果第二个元素设置更小的font-size
呢?
span:last-child {
font-size: 50px;
}
这听起来很奇怪,但默认基线对齐可能导致更高的line-box,如下图所示。我提醒你,line-box的高度是从它的子元素最高点和最低点计算。
有一个观点可以得到支持,那就是line-height
设置不带任何单位的值,但有时你需要做一个完美的Vertical-rhythm。说实话,不管你选择什么,你总是会有困难的。
看看另一个例子。<p>
元素的line-height
值设置了200px
,并且包含了一个<span>
元素,这个<span>
元素继承了<p>
元素的line-height
。
<p>
<span>Ba</span>
</p>
p {
line-height: 200px;
}
span {
font-family: Catamaran;
font-size: 100px;
}
line-box有多高?我们期望的是200px
,但如果不是,我们得到的又是什么?这里不同的是<p>
元素有自己的字体(默认是serif
)。<p>
和<span>
之间的基线可能是不同的,因此line-box的高度是高于预期的。这是因为浏览器给每个line-box计算都是开始于一个任意字符。规范中称之为strut。
一个看不见的角色,但的确是会有可见的影响。
就我自己一些经历,我们将面临同样的问题,那就是兄弟元素。
基线对齐是完了,但vertical-align:middle
可以拯救它们?可以阅读规范:
Middle “aligns the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent”.
基线的比例不同,以及x-height比例,所以中间对齐是不可靠。最坏的情况下,在大多数的情况下,中间就从来没有真的中间过。这里面有太多的因素参与其中,使用CSS是不能设置这些因素的(x-height,ascender和descender比例等)。
它有四个值,这可能在某些情况下是有用的:
vertical-align: top | bottom
和line-box的顶部或底部对齐vertical-align: text-top | text-bottom
和内容区域的顶部或底部对齐
注意了,在所有情况下它都是在虚拟区域中,所以是看不见的高度。看看这个简单的示例,使用vertical-align:top
,看不见的line-height
可能产生一些很奇怪的结果。
最后,vertical-align
还能接受数值,提高或降低盒子的基线。最后一个选项可以派上用场。
CSS 无所不能
我们已经讨论过了line-height
和vertical-align
在一起是如何工作,但现在的问题是如何使用CSS来控制字度的度量指标?简短的回答:没有。即使真的如此,我也想我们应该可以做些什么?那么有关于字体度量,我们应该能够做些什么?
例如,如果我们想要给文本使用Catamaran字体,可以把其capital高度扩展到100px?通过一些数学计算,似乎可行。
首先设置度量字体的五个自定义属性,然后计算font-size
,从而得到capital高度是100
。
p {
/* font metrics */
--font: Catamaran;
--capitalHeight: 0.68;
--descender: 0.54;
--ascender: 1.1;
--linegap: 0;
/* desired font-size for capital height */
--fontSize: 100;
/* apply font-family */
font-family: var(--font);
/* compute font-size to get capital height equal desired font-size */
--computedFontSize: (var(--fontSize) / var(--capitalHeight));
font-size: calc(var(--computedFontSize) * 1px);
}
很简单,不是吗?但如果我们想要让文本在可视区居中,让剩余的空间均分在"B"字的顶部和底部,应该怎么做呢?为了达到这一目的,我们必须基于ascender和descender比例计算出vertical-align
。
首先,计算line-height:normal
和内容区域的高度。
p {
…
--lineheightNormal: (var(--ascender) + var(--descender) + var(--linegap));
--contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}
这时,我们需要:
- 大写字每底部距离底部边缘的距离
- 大写字母顶部距离顶部边缘的距离
像这样:
p {
…
--distanceBottom: (var(--descender));
--distanceTop: (var(--ascender) - var(--capitalHeight));
}
我们现在可以通过距离乘以font-size
计算出vertical-align
。
p {
…
--valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
vertical-align: calc(var(--valign) * -1px);
}
最后,我们设定所需的line-height
和计算它,保持一个垂直对齐:
p {
…
/* desired line-height */
--lineheight: 3;
line-height: calc(((var(--lineheight) * var(--fontSize)) - var(--valign)) * 1px);
}
添加一个图标和字母"B"垂直对齐,现在很容易就能做到:
span::before {
content: '';
display: inline-block;
width: calc(1px * var(--fontSize));
height: calc(1px * var(--fontSize));
margin-right: 10px;
background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');
background-size: cover;
}
示例的地址可以点击这里。
注意:这个测试只是出于演示目的。你不能依赖于此。如果字体不加载,备用字全有可能具有不同的字体度量参数,它就没法正常工作了。
在一部分示例中,大家看到很有以
--
开头的,这是CSS的原始变量,也称之为CSS自定义属性。如果在此之前你从未接触过这方面的内容,建议你先点击这里进行了解。
总结
这篇文章我们学到了什么:
- 行内格式化上下文真的很难理解
- 所有行内元素都有两个高度
- 内容区域(content-area)基于字体的度量参数
- 虚拟区域(virtual-area)就是
line-height
- 这两个高度是无法可视的(如果你通过开发者工具,你可以看到)
line-height:normal
是基于字体度量参数line-height: n
有可能创建一个虚拟区域比内容区域更小vertical-align
不是很可靠- 一个line-box的高度计算是基于它的子元素的
line-height
和vertical-align
属性 - 我们没有办法直接通过CSS来获取或设置字体的度量参数
- 未来可能会有一个垂直对齐的规范来解决这些看似问题的问题:Line Grid Module
相关资源
- 获取字体度量参数的工具:FontForge,opentype.js
- 在浏览器中计算
line-height:normal
和一些比例 - Ahem,一个特殊字体,帮助我们如何理解它怎么工作
- 一个更深,更透彻阐述行内格式化上下文
- 两份详细介绍
line-height
的PPT:CSS line-height和Deep dive line-height
本文根据@iamvdo的《Deep dive CSS: font metrics, line-height and vertical-align》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align。
如需转载,烦请注明出处:http://www.w3cplus.com/css/css-font-metrics-line-height-and-vertical-align.html