本文翻译自《Deep dive CSS: font metrics, line-height and vertical-align》
line-height
和 vertical-align
都是很简单的CSS属性,简单到我们相信自己能够完全理解它们怎么工作以及怎样使用它们。其实了解这两个属性不是件易事,因为它们在CSS中起着一个鲜为人知却十分重要的作用:内联格式化上下文。
例如,line-height
可以被设为长度或者无单位的值,默认值为 normal
。但normal值具体是多少呢?通常应该是1或者1.2,甚至 CSS规范 也没有明确。我们知道无单位的 line-height
是相对于字体大小来确定的,但问题是 font-size: 100px
对于不同的字体来说渲染的结果不尽相同,这时 line-height
的表现是否也是不一样呢?是否真的是介于1到1.2之间呢?还有 vertical-align
会对 line-height
有什么影响呢?
先从 font-size
谈起
下面是简单的HTML代码,一个 <p>
标签包含三个不同 font-family
的 <span>
:
<p>
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
</p>
p { font-size: 100px }
.a { font-family: Helvetia }
.b { font-family: Gruppo }
.c { font-family: Catamaran }
不同的字体使用相同的 font-size
导致不同的元素高度
即使我们已经知道到这个结果,但是为什么 font-size: 100px
不是创建高为100px的元素呢?测量到不同字体的不同高度:Helvetica:115px,Gruppo:97px,Catamaran:164px
尽管乍一看起来有点奇怪,但完全可以预料。原因就在字体本身。它的工作原理如下:
字体定义它的 em-square(也被称作“EM size”或者“UPM”),在一个字体中,每个字符都放置在一个方块空间容器内。方块使用相对单位,通常为1000个单位,也可以是1024或者2048或者其他。
基于它的相对单位,设置字体的指标(ascender 顶高、descender 底深、capital height 大写高度、x-height x高度等等,顶高和底深分别在基线的上下)。注意有些值是可以大于em-square的。
在浏览器中,相对单位会被缩放以适应所需字体的大小
用 FontForge 查看Catamaran字体的指标
em-square设置为1000
顶高为1100,底深为540。经过一些测试,浏览器在Mac OS上使用 HHead Ascent/Descent,Windows上使用 Win Ascent/Descent(这些值可能会不一样)。我们也可以注意到 capital height 为680以及 x height 为485。
这意味着Catamaran字体设置 font-size: 100px
时,其在1000单位的em-square中使用1100 + 540个单位,得到实际高度164px。此结果定义元素的 content-area 的高度。你可以把 content-area 视为 background
属性应用的区域。
你也可以看出大写字母高68px(680单位)以及小写字母高49px(485单位)。1ex
= 49px、1em
= 100px,而不是164px(幸运的是,em
是基于 font-size
而不是计算高度)
在更深入之前,简单介绍它涉及的内容。当一个 <p>
元素被渲染,根据它的宽度,它可以由若干行组成。每行由一个或多个 line-box 的内联元素(HTML标签或文本内容的匿名内联元素)组成。line-box 的高度取决于它的子元素高度。因此,浏览器计算每个内联元素的高度,从而计算线框的高度(从其子元素的最高点到最低点)。最终 line-box 的高度(默认)足以包含其所有子元素。
每个HTML元素其实就是一堆 line-box,如果你知道了该元素每个 line-box 的高度,你就可以知道这个元素的高度了。
更新之前的HTML代码:
<p>
Good dedign will be better.
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
We get to make a conswquence.
</p>
这会生成三个 line-boxs:
第一个和最后一个包含着一个匿名内联元素(文本内容)
第二个包含两个匿名内联元素以及三个
<span>
很明显看到第二个 line-box 高于其他的,由于计算其子元素的 content-area,更具体来说是那个使用了Catamaran字体的子元素。
创建 line-box 的难点在于我们无法观察其真实区域,也无法使用CSS来控制它。即使应用 ::first-line
也没有给第一个 line-box 的高度产生任何视觉变化。
line-height
到目前为止,我介绍了两个概念:content-area 和 line-box。注意,我是说 line-box 的高度是取决于它的子元素的高度,而不是它子元素的 content-area 高度。两者差别很大。
一个内联元素拥有两个不同的高度:content-area 高度以及 virtual-area 高度(virtual-area 这个词是自定义的,因为这个区域的高度是不可见的)。
content-area 由字体的指标定义
virtual-area 就是
line-height
,它用于计算 line-box 的高度
可以看出 line-height
并不是基线之间的距离。
virtual-area 和 content-area 的差值平均分配到 content-area 的顶部和底部。 因此 content-area 总是位于 virtual-area 的中部
line-height
(virtual-area)可以等于、大于或者小于 content-area。如果 virtual-area 的值更小,那么差值就为负数,line-box 视觉上就会小于其子元素。
其他类型的内联元素:
replaced-inline-element(
<img>
、<input>
、<svg>
等等)inline-block
以及所有的inline-*
元素参与特定格式化上下文的内联元素(例如:包含在 flexbox 里以及所有的 flex items 都是 blocksified)
对于这些特殊的内联元素,高度的计算基于其 height
、margin
以及border
属性。如果 height
为 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。用于字体绘制
指标 Ascent/Descent:ascender 为1100,descender 为540。用于 content-area 的高度
指标 Line Gap(行间隙),通过加上 Ascent/Descent 的值计算出
line-heigh: normal
的值
在这个例子中,Catamaran 字体定义 line gap 值为0,所以 line-height: normal
等于 content-area 的1640单位或者1.64
为了方便对比,Arial 字体的 em-square 为2048单位,ascender 为1854,descender 为434,line gap 为67。所以当 font-size: 100px
时,content-area 的高为112px((1854 + 434) / 2048 100px),line-height: normal
为115px((1854 + 434 + 67) / 2048 100px)。不同的字体指标可能不一样,由字体设计人员设置。
很显然,将 line-height 设为无单位值可能达不到预期效果。无单位值是相对于 font-size
而不是 content-area,当 virtual-area 的高度小于 content-area 的高度会导致很多问题。例如 line-height: 1
时可能会导致 line-box 的高度小于 content-area 的高度:
计算 line-box 的一些小细节
对于内联元素,
padding
以及border
增大了 background 的区域,但不会增加 content-area 的高度(也不会增加 line-box 的高度)。因此,content-area 不总是你在屏幕上看到的。margin-top
和margin-bottom
也不会对其造成影响。对于 replaced-inline-element、
inline-block
以及 blocksified 内联元素:padding
、margin
和border
会增加height
,所以会增加 content-area 以及 line-box 的高度。
vertical-align
到目前为止,我还没有提及 vertical-align
属性,尽管它是是计算 line-box 高度的重要因素,甚至可以说它在内联格式化上下文起着主导作用。
它的默认值是 baseline
,字体指标 ascender 和 descender 决定了 baseline 的位置。ascender 和 descender 的占比很少五五开,例如对于兄弟元素,可能导致意想不到的效果。请看如下代码:
<p>
<span>Ba</span>
<span>Ba</span>
</p>
p {
font-family: Catamaran;
font-size: 100px;
line-height: 200px;
}
一个 <p>
元素包含着两个继承了 font-family
、font-size
和固定的 line-height
的兄弟元素 <span>
。它们的基线相同,而且 line-box 的高度也等于它们的 line-height
。
如果第二个元素的 font-size
改为比较小的呢?
span:last-child {
font-size: 50px;
}
如下图所示,默认的基线对齐可能会导致更高的 line-box。正如前面所说的 line-box 的高度取决于它的子元素的最高点和最低点。
这可能是 line-height
支持无单位值的论据,但有时你需要确切的值才能完美实现垂直居中。其实无论你选择那种方式,你总会遇到内联对齐问题
另外一个例子,设置一个 <p>
标签 line-height: 200px
,其子元素 <span>
继承它的 line-height
<p>
<span>Ba</span>
</p>
p {
line-height: 200px;
}
span {
font-family: Catamaran;
font-size: 100px;
}
line-box 的高度是多少?期望是200px,但结果并不是。问题在于 <p>
拥有它自己的、不同的 font-family
(默认是 serif
)。<p>
和 <span>
的基线可能会不一样,因此结果的高度高于预期的高度。出现这个结果的原因是浏览器在对齐 line-box 的子元素时,以一个零宽度的字符开始,这个零宽度字符称作支柱(strut)。
根据规范,middle
使元素的中部与父元素的基线加上父元素的 x-height 的一半对齐。基线比率不同,x-height 的比率也不同,所以 middle
对齐不是真的“在中部”。涉及的因素(x-height,ascender/descender 比率等等)太多,而且这些因素不能通过CSS设置。
不过 vertical-align
还有四个其他属性在某些情况下或许有用。
vertical-align: top / bottom
与 line-box 的顶部或底部对齐vertical-align: text-top / text-bottom
与 content-area 的顶部或底部对齐
但要注意的是在所有情况下,设置该属性的元素都会与父元素的 virtual-area 对齐,也就是不可视高度。以下是一个使用 vertical-align: top
的简单例子。不可视的 line-height
可能会导致奇怪的结果。
最后,vertical-align
还接受具体的长度值,表示使元素的基线对齐到父元素的基线之上的给定长度,可以是负数。
令人惊叹的CSS
我们已经了解了 line-height
和 vertical-align
之间是如何工作的,也知道字体指标不可以使用 CSS 控制。但是每种字体指标是固定值,或许可以用来进行某些运算。
例如,我们使用 Catamaran 字体,要使其大写字母高度恰好为100px,该怎么办?
首先,我们将所有字体指标设置为CSS自定义属性,然后计算高度为100px的大写字母所需的 font-size
。
p {
/* 字体指标 */
--font: Catamaran;
--fm-capitalHeight: 0.68;
--fm-descender: 0.54;
--fm-ascender: 1.1;
--fm-linegap: 0;
/* 期望的大写字母高度的 font-size */
--capital-height: 100;
/* 应用 font-family */
font-family: var(--font);
/* 计算 font-size,使大写字母高度等于100px */
--computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
font-size: calc(var(--computedFontSize) * 1px);
}
接下来我们想要文本在可视区域的中部,剩下的空间平均分布在大写字母“B”的顶部和底部。所以我们需要基于 ascender/descender 比率来计算 line-height
。
首先计算 line-height: normal
和 content-area 的高度:
p {
/* ... */
--lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));
--contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}
然后我们需要计算:
大写字母底部到底部边缘的距离
大写字母顶部到顶部边缘的距离
p {
/* ... */
--distanceBottom: (var(--fm-descender));
--distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
}
现在我们可以计算 vertical-align
,它的值等于以上两个距离的差值乘于 font-size
。(该值需应用到一个行内子元素)
p {
/* ... */
--valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
vertical-align: calc(var(--valign) * -1px);
}
最后,我们设置所需的行高并计算它,同时保持垂直居中
p {
/* ... */
--line-height: 3;
line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
}
现在很方便地添加一个与字母“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;
}
注意这些例子仅供演示,不应直接应用,原因:
除非字体指标是常量,否则浏览器的计算结果可能不一样
如果未加载字体,则回退的字体可能具有不同的字体指标
总结
这篇文章讲了:
内联格式化上下文有点难以理解
所有内联元素拥有两个高度:
content-area(基于字体指标)
virtual-area(
line-height
)
line-height: normal
准确值的计算基于字体指标line-height: 1
可能会创建一个高度小于 content-area 的 virtual-areavertical-align
并不是真的“在中部”line-box 的高度的计算基于其子元素的
line-height
和vertical-align
无法使用 CSS 获取或设置字体指标