Line-height
和 vertical-align
是比较简单的CSS属性,以至于我们大多数人都觉得完全理解这两个属性是如何工作以及如何使用它们。实际并非如此。这两个属性非常复杂,也许可以说是最难理解的属性了。CSS有一个鲜为人知的特性:内联元素格式化。这两者恰好在这个特性上起着重要作用。
例如line-height
可以是一个长度或者是一个没有单位的数值注1,但它的默认值是normal
。normal
又代表什么呢?我们把它当作是1或者是1.2,甚至连CSS spec都没有讲清楚这个值是什么。我们知道,无单位的line-height
是 font-size
相关联的,但问题是font-size:100px在不同字体时表现是不一样的,那line-height
是相同还是不同的? 它的值真的在1到1.2之间吗? 还有 vertical-align
,它对line-height
有什么影响?
因此我们需要深入研究一下这个不那么简单的CSS机制。
让我们先研究下font-size
看看这段简单的HTML代码,包含3个,每个都有不同的 font-family
.
1
2
3
4
5
|
<p>
<span
class
=
"a"
>
Ba
</span>
<span
class
=
"b"
>
Ba
</span>
<span
class
=
"c"
>
Ba
</span>
</p>
|
1
2
3
4
|
p
{
font-size
:
100px
}
.a
{
font-family
:
Helvetica
}
.b
{
font-family
:
Gruppo
}
.c
{
font-family
:
Catamaran
}
|
使用同样的font-size
和不同的font-family
会产生不同高度的元素:
图1.不同font-family, 相同font-size, 高度不同
即便我们意识到这点,为什么font-size: 100px
不能产生相同高度的元素呢?我测试过一些字体集,并得到以下值 Helvetica: 115px, Gruppo: 97px, Catamaran: 164px。
图2.font-size为100px的元素高度从97px到164px不等
第一眼看上去有点不可思议,但实际上这就是这样的。原因在于字体本身。它的机制是这样:
- 一个字体定义了它的em-square(或UPM,即每个em的单位)。也就是一个容器,每个字符将被绘制在容器里。这个正方形使用相对单位,通常设置为1000单位,但也可以是1024,2048或其他任何值。
- 根据字体的相对单位,设置字体的其他度量值(升部,降部,大写高度,x字高等等)。请注意,某些值可能会超出这个方形容器。
- 浏览器为了适应所需的字体大小,会缩放相对单位。
以Catamaran字体为例,并在[FontForge](https://fontforge.github.io/en-US/)中打开,看一看其中的各项指标:
- em-square是1000个单位的。
- 升部是1100,降部是540。在一些测试后,看上去浏览器在Mac OS上的HHead Ascent/ Descent值,Windows上的Win Ascent/Descent值(这些值可能是不同的!)。 还有,Capital Height(大写高度)是680,X height(X字高)是485。
图3: 使用FontForge看到的字体各个度量值
这意味着Catamaran在1000个单位的容器中就用了1100 + 540个单位,因此使用这个字体时,如果设置font-size: 100px
,那么实际高度就是164px。这个计算出的高度定义了一个元素的content-area,内容区域,我将在本文的其余部分引用这个术语。您可以将content-area理解为background
属性应用的区域注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) 我们还可以预测,大写字母是68px(680单位),小写字母(X字高)高49像素(485单位)。 因此,1ex
= 49px和1em
= 100px,而不是164px(谢天谢地,em
单位是基于font-size
,而不是计算后的高度)。
图4: Catamaran字体:UPM-每单位em数-和像素值在使用font-size:100px时值相同。
再更深入之前,我再简要介绍一下这一机制。 当一个
元素在屏幕上呈现时,根据它的宽度定义,可以产生许多行。 每行由一个或多个内联元素(HTML标签或文本内容的匿名内联元素)组成,称为line-box。 line-box的高度由其子节点的高度决定。 因此,浏览器会计算计算line-box(从其子节点的最高点到子节点的最低点)的高度,由此便有了每个内联元素的高度。 因此(在默认情况下),line-box总是能够容纳其所有子节点。
每个HTML元素实际上是一堆line-box的集合。如果你知道每个line-box的高度,你就知道一个元素的高度。
如果我们把之前的代码改成下面这样:
1
2
3
4
5
6
7
|
<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>
|
就会生成3个line-box:
- 第一行和第三行包含一个匿名的内联元素(文本内容)。
- 第二行包含两个匿名的内联元素,以及3个
.
图5:这个
段落(黑色边框)由包含内联元素(实体边框)和匿名内联元素(虚线边框)的线框(白色边框)共同组成。
我们可以很清楚地看到,第二个line-box比其他的高,因为其子节点的content-area更高,准确的说,是因为子节点使用了Catamaran字体。
问题在于line-box的创建的过程是黑盒的,也不能使用CSS来控制。即便使用:: first-line
也不能让我们设定第一个行内元素的高度。
line-height
延伸出的问题
现在为止,我介绍了两个概念:content-area和line-box。如果你看的很仔细,你就会发现我虽然说过一个line-box的高度是根据它的子节点的高度计算出,但我并没有说它的子节点content-area的高度。这两者有很大不同。
可能这听起来很奇怪,一个内联元素有两个不同的高度:content-area 的高度和virtual-area 高度(我发明了这个术语virtual-area,因为这个高度不可见,但你在现在的规范里找不到这个词)。
- (如前所述)content-area 的高度由字体度量值来定义。
- virtual-area 高度就是
line-height
,它就是用来计算line-box的高度**
图6:内联元素的两种不同高度
也就是说,人们通常的看法,即line-height
是基线之间的距离,这一观点是错误的。在CSS中,这并不成立注3。
图7:CSS里,line-height并不是基线直接的距离。
virtual-area和content-area之间的计算高度差异称为行距。 这个行距一半在content-area的顶部,另一半在底部。 因此content-area始终位于virtual-area的中间。
根据行距的计算值不同,line-height
(virtual-area)可以等于,大于或小于content-area。 在比virtual-area更小的情况下,行距为负,所以line-box在视觉上小于其子节点。
还有其他几种内联元素。
- 被替换的内联元素,(
,
,
等等.)
inline-block
和所有 allinline-*
的元素- 参与特定格式化内容的内联元素(例如,在flexbox元素中,所有flex元素都是blocksified)
对于这些特定的内联元素,高度是根据height
,margin
和border
这些属性计算出的。 如果height
是auto
,那么就使用line-height
,content-area严格等于line-height
。
图8:内联替换的元素,inline-block、inline-*和blocksified的元素的内容区域等于其高度或行高
不过,我们面临的问题仍然是:line-height
的normal
值是多少? 可以在字体度量值中找到问题的答案,也就是content-area的高度计算。
让我们回到FontForge。 Catamaran的em单位是1000,但是我们看到各种不同升部和降部的值:
- 生成的升部/降部: 升部是770,降部是230。用于绘制字符。(表OS/2)
- 度量的升部/降部: 升部1100,降部是540。用于绘制 content-area的高度
- 度量线距。在
line-height: normal
时使用,在升部和降部之间的距离(表“hhea”)。
在这个例子中,Catamaran字体定义了0单位线距,所以line-height:normal
将等于content-area,它是1640单位,或1.64。
相比而言,Arial字体定义了2048个单位的大小,1854的升部,434的降部和67的线距。 这意味着font-size:100px
会生成一个112px(1117个单位)的content-area和一个115px(1150个单位或1.15个)的line-height:normal
。所有这些度量值都是字体特有的,由字体设计者设置。
很明显,设置line-height: 1
并不好。我要提醒你,无单位值是和font-size
相关的,而不是content-area相关,而正是处理比content-area小的virtual-area的情况才是许多问题的起源。
图9:line-height: 1
,产生的line-box比content-area更小。
但这还不仅仅是line-height:1
的问题。我的电脑上安装了1117种字体(是的,我安装了所有的字体从Google Web字体,1059种字体,大约95%,计算出的line-height
都大于1。他们的line-height
从0.618到3.378不等。你没看错,是3.378!
line-box计算的小细节:
- 对于内联元素,
padding
和border
会增加背景区域,但不会增加content-area的高度(也不是line-box的高度)。 因此content-area并不总是在屏幕上看到的内容。margin-top
和margin-bottom
无效。 - 对于替换型的内联元素,
inline-block
和blocksified inline元素来说,padding
,margin
和border
增加的是height
,也就是content-area和line-box的高度。
##vertical-align
:一个统领全局的属性#
之前我并没有提到vertical-align
属性,尽管它是计算line-box高度的一个重要因素。 甚至可以说**vertical-align
可能在内联内容格式化上有着重要作用。
vertical-align
的默认值是baseline
。你还记得字体指标里升部和降部吗? 这些值确定基线在哪里,也确定他们的比例。由于升部与降部的比例很少为50/50,因此可能会产生一些比如对兄弟节点的影响。
还是从代码看起:
1
2
3
4
|
<p>
<span>
Ba
</span>
<span>
Ba
</span>
</p>
|
1
2
3
4
5
|
p
{
font-family
:
Catamaran
;
font-size
:
100px
;
line-height
:
200px
;
}
|
这个
元素含有两个
互为兄弟节点。他们继承了
font-family
, font-size
以及固定 line-height
的属性。他们的基线会相同,并且这两个元素的line-box高度都和他们的line-height
行高相同。
图10: 相同的字体,基线相同,万事大吉
但如果第二个元素的font-size
变小了呢?
1
2
3
|
span:last-child
{
font-size
:
50px
;
}
|
听上去这可能很奇怪,默认基线对齐方式可能会导致一个更高(!)的line-box,如下图所示。你需要了解的是,line-box的高度是从其子节点的最高点到其子节点的最低点计算出来的。
图11:较小的子元素会使line-box高度增加
这可能是[尽量使用line-height
无单位值的依据](http://allthingssmitty.com/2017/01/30/nope-nope-nope-line-height-is-unitless/),但有时我们也需要固定值来[创建一个完美的垂直对齐的用例](https://scotch.io/tutorials/aesthetic-sass-3-typography-and-vertical-rhythm#baseline-grids-and-vertical-rhythm) 其实,无论你怎么选,都会遇到内联元素对齐的麻烦。
让我们看看另一个例子。一个line-height:200px
的
标签,包含一个
元素,子元素继承了
line-height
的值。
1
2
3
|
<p>
<span>
Ba
</span>
</p>
|
1
2
3
4
5
6
7
|
p
{
line-height
:
200px
;
}
span
{
font-family
:
Catamaran
;
font-size
:
100px
;
}
|
line-box有多高? 我们的期望值应该是200px,但事实并非如此。其中的问题是
有自己的字体,不同于
font-family
(默认为serif
)。
和
之间的基线可能会有所不同,因此line-box的高度比我们预期的高。这是因为浏览器进行计算时,会以每行line-box的一个零宽度字符开始,这一规范称为strut。
一个看不见的字符,带来看得见的效果
我们再回过头来看一下之前提到兄弟节点的问题。
图12:每个子元素都是对齐的,因为其line-box都从一个看不见且没有宽度的字符计算出的。
但是基线对齐就不管用了。但是用vertical-align: middle
可以解决这个问题么?规范里提到, middle
意思是垂直方向上父节点的基线加上一半的x子高的总高度的中部对齐。基线比例是不同的,X子高的比例一样,所以middle
对齐并不可靠。更糟糕的是,在大多数情况下,middle
绝对不会在正中间。有太多因素会影响对齐,无法通过CSS设置这些因素(x字高,升部/降部的比例等等)。
附注:还有4个其他值,在某些情况下可能有用:
vertical-align:top
/bottom
对齐到line-box的顶部或底部vertical-align:text-top
/text-bottom
对齐到content-area的顶部或底部
图13:Vertical-align对应四种值的情况
注意,在所有的情况下,都会对齐virtual-area,也就是那个不可见的高度。看一下这个简单的例子,使用vertical-align:top
。不可见的line-height
可能会产生奇怪但并意料之中的结果。
图14:垂直对齐可能会产生奇怪的结果,但是当你把行高可视化后,结果其实是意料之中的
最后,vertical-align
也可以是提高或降低与基线相关的数值的值。最后这个值可能会派上用场。
CSS 棒极了
我们已经讨论了line-height
和 vertical-align
的工作机制,那么问题来了:字体度量值是否可以用CSS控制?简单来说:不能,哪怕我十分希望可以。
不过无论如何,我们可以尝试下。字体度量值是常量,所以我们应该能够利用一下。
举个例子,假如说我们要使用Catamaran字体的文字,让文字高度高达正好是100像素。似乎是可行的,计算一下好了。
首先,我们用CSS自定义属性设置字体指标注4,然后通过计算font-size
以获得100px的高度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
)
;
}
|
图15:大写字符的高度正好是100px
很简单,不是吗? 但是,如果我们希望做到视觉上文本居中的效果,那么剩余的空间应该是需要平均分配在“B”字母的顶部和底部?为了实现这一点,我们必须基于升部/降部的比例来计算 vertical-align
。
首先,计算line-height:normal
和content-area的高度:
1
2
3
4
5
|
p
{
…
--lineheightNormal
:
(
var
(
--fm-ascender
)
+
var
(
--fm-descender
)
+
var
(
--fm-linegap
)
)
;
--contentArea
:
(
var
(
--lineheightNormal
)
*
var
(
--computedFontSize
)
)
;
}
|
然后,我们需要:
- 大写字母底部到底部边缘的距离
- 大写字母顶部到顶部边缘的距离
就像这样:
1
2
3
4
5
|
p
{
…
--distanceBottom
:
(
var
(
--fm-descender
)
)
;
--distanceTop
:
(
var
(
--fm-ascender
)
-
var
(
--fm-capitalHeight
)
)
;
}
|
现在我们就可以计算vertical-align
的值了,也就是距离乘以计算后font-size
之间和底部的差值。 (我们必须将此值应用到内联子元素上)。
1
2
3
4
5
6
7
|
p
{
…
--valign
:
(
(
var
(
--distanceBottom
)
-
var
(
--distanceTop
)
)
*
var
(
--computedFontSize
)
)
;
}
span
{
vertical-align
:
calc
(
var
(
--valign
)
*
-1px
)
;
}
|
最后,我们要设置所需的line-height
,并计算如何保持垂直对齐:
1
2
3
4
5
6
|
p
{
…
/* desired line-height */
--line-height
:
3
;
line-height
:
calc
(
(
(
var
(
--line-height
)
*
var
(
--capital-height
)
)
-
var
(
--valign
)
)
*
1px
)
;
}
|
图16: 具有不同行高的例子。但文字都在中间。
现在再把一个和字符“B”等高的图标加进去就很容易了
1
2
3
4
5
6
7
8
9
|
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
;
}
|
图17: 图标和字符等高
请注意,此测试仅用于演示目的。你不能依赖这一方法。原因有很多:
- 除非字体指标是不变的,浏览器中的计算不是 ¯_(ツ)_/¯
- 如果字体未加载,则默认字体可能具有不同的字体度量,并且处理多个值的计算逻辑将很快变得难以管理。
供你带走的部分#
现在我们学习到了:
- 内联元素的格式化真的很难理解
- 所有的内联元素都有两种高度
content-area(基于度量值) virtual-area (line-height
行高) ** 毫无疑问,这两个高度都不能可视化。(如果你是一个devtools开发人员并且能够解决可视化问题,那就太棒了)
line-height: normal
是基于字体度量值的。line-height: n
可能会创造一个比content-area更小的virtual-areavertical-align
不可靠。- 一个line-box的高度是基于它的子节点
line-height
和vertical-align
属性来计算的 - 没有什么好办法能用CSS简单设置字体指标
但我依然爱CSS:)
资源
- 获取字体度量值: FontForge, opentype.js
- 计算
line-height: normal
的值,以及一些比例 - Ahem, 一种帮助你知道字体是如何构成的字体库
- 另一篇更深入的教程,解释 内联元素格式化
- 有一个帮助设置垂直对齐的未来规范:Line Grid module
- Font Metrics API Level 1, 一个有趣点子的合集