文字特性与CSS单位
《寻根问底之line-height与vertical-align》这篇文章详细地剖析了line-height
与vertical-align
这两个CSS属性共同作用的机理。为了解释一些属性的定义和规范,文中简单介绍了一点文字排版和文字特性的知识。但对于什么是x-height
,什么是ascender
、为何不同的字体line-height:normal
的值不一样等细节的内容并没有做详细的说明。深入的去理解文字设计以及排版的特性,有利我们更好的去理解CSS中一些属性定义以及排版规范的理解,进而有利我们写出更符合规范的CSS代码,更好的去解决一些网页排版中的疑难杂症。本文旨在通过详细地介绍文字的设计特性以及文字排版的相关知识,加深我们对line-height
以及vertical-align
等CSS属性的理解。
# 一、字体类型
# 1. 衬线字体与无衬线字体
字体衬线设计的初衷是通过更清晰的标明笔触的末端,提高辨识度,进而提高阅读的速度。衬线通常是笔画边缘的一小段修饰,它强调了每一个字符笔画的开始和结束,笔触之间的粗细分明,非常有利于阅读。相比之下,无衬线字体则更加简洁醒目,笔画之间的粗细差别不明显。通常情况下对于大段的文本印刷和排版,人们习惯于使用衬线字体,而对于较为简短,或者需要特殊表现的文本(如标题、海报等)人们更倾向于采用无衬线字体。我们平时使用的英文字体Arial就属于无衬线字体,Times New Roman就属于衬线字体。我们使用的中文字体中,黑体就属于无衬线字体,而宋体就属于衬线字体。几种字体的表现如下图:
尽管我们认为对于大段的文本来讲,使用衬线字体更具可读性,但是鉴于电子设备分辨率等因素,在网页设计中,某些场景(如较小字号的段落)我们大部分情况下是采用无衬线字体。因为无衬线字体的笔画简单,在这些场景中反而更具备可读性。
# 2. 等宽字体
顾名思义,等宽字体是指同一字体下,字符宽度相同的字体。与等宽字体对应的是比例字体。
在传统西文印刷中,比例字体是可以提高单词的可读性的。但是早期的电脑显示器、打字机等因为技术的局限,无法进行字母宽度的比例调整,所以每个字符都被设计成了一样的宽度,这样就形成了等宽字体。在等宽字体中,i、l等字符两侧的空白会相对较多,而m,w等字符则相对较为紧密。常见的等宽字体有Monospace
、Consolas
。以下为等宽字体和比例字体在相同字符数下的表现结果:
笔记
等宽字体通常在代码源码格式化时使用
# 二、字体设计
字体设计通常考虑的不是单个字符的设计,每一种字体设计都要考虑每一个字符与其他字符甚至与其他字体一起工作的表现形式。为了使字体更加优雅、更好的应用于排版或者突出某种特性,字体设计通常会考虑字体结构、粗细、大小、宽度、倾斜角度、易读性和字符重心等因素。
# 1. 字体结构
字体结构,是形成特定的字形的基础笔画。这只是一个字体设计时每一个字符具体的字体形状。通常字体结构不包含字体的的其他修饰属性,如粗细、笔画端点类型等因素。如果把字体的每一个字符都当作一个有血有肉的物体,那么字体的结构可以认为是物体的基础骨骼,而衬线、粗细等修饰属性可以当作是字体的皮肤、血肉等部分。
如上图,同样的两个字符,不同字体下,其基本的形状可以相差很远,其中白色的部分就是字体结构最基本的“骨骼”,有了基本的结构,就可以对字体进行其他的表现形式的修饰,如字体的粗细、末端形状等。
# 2. 字体度量 重要
字体大小(font-size
)是CSS中涉及比较多的一个属性。我们在使用这个属性的时候也许并没有真正去关心过设置的属性到底是对文字的哪一部分做了设置。但其实了解其中的细节,非常有利于我们对一些常见的问题做一些分析。本小节的内容是全文的重点,下面将会花费较大的篇幅来讨论这个问题。
本小节绝大部分内容翻译自文章:《Deep dive CSS: font metrics, line-height and vertical-align》 (opens new window)
2.1 EM Square
先来了解一个概念,叫做EM Square
,也叫UPM
或者EM size
。它是西文排版中的一个概念,指在一个字体中,每一个字符都是被放置在一个特定的空间容器里。在传统金属活字印刷中(如下图),这个容器就是每个字符实际的金属块(字模),每一个字符都可以被整齐的放置到金属块中。字模的高度被称为em
,起源于大写字符"M"的宽度,这个字模的比例会被设定为正方形,因此才有了EM Square
的称呼。在数码排版中,em是一个数字化的空间定义,是一个相对的单位,会根据实际的字体大小进行缩放。在OpenType字体中,通常是1000个单位,而在TrueType字体中,通常被约定为2的n次幂个单位,比如1024、2048、4096个单位。例如当字体被设置为20px时,在OpenType字体中,1000个单位就是20个px。
图片来源:http://designwithfontforge.com/en-US/The_EM_Square.html (opens new window)
2.2 字体度量属性
字体是有很多度量(metrics)的属性的,比如基线、中线、行高、行距、升部等。在了解字体的大小如何定义之前,需要先弄明白这些度量属性。如下图:
上图原图来源于维基百科《x-height》 (opens new window),为了方便理解做了少许的修改与标识。
baseline
:基线,指在文字排版时,小写字母x底部所在的位置cap-height
:大写字母的高度,表示位于基线上的大写字母的高度,其顶部所在的线为cap line
x-height
:baseline
与mean line
的距离,指的是小写字母x
的高度desender line
:下降线,表示小写字母中扩展到底部的部分所在的线,比如字母q、p等底部所在的位置,扩展的这一部分叫做desender
ascender line
:上升线,表示小写字母中超出字母x顶部的部分所在的线,比如字母h顶部所在的位置,超出的这一部分叫做ascender
。为了辨识性,asender的高度有可能会比cap-height
的高度大一点点,如上图h
比S
会高一点点。line gap
指上一个desender
底部到下一个ascender
的距离。这个距离不一定要有,有些字体就是0,比如下文介绍的Catamaran字体
的line gap
就为0。
了解了以上这些关于字体度量的属性和概念,就可以借助一些示例来对font-size
属性做一个详细的探讨了。下面准备一个例子:
<p>
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
</p>
2
3
4
5
p { font-size: 100px; }
.a { font-family: Helvetica; }
.b { font-family: Gruppo; }
.c { font-family: Catamaran; }
2
3
4
以上代码简单的将三个span
标签包裹在一个p
标签内,并设置为三种不同的字体,文字大小均为100px。运行后效果如下:
深色背景为三个span
实际占据的高度,可以清楚的观察到,不同字体下同样大小的font-size
,渲染后的高度是不一样的。通过测量,实际的渲染高度如下图所示:
虽然这个结果咋一看有点难以理解,但实际上并奇怪。出现这种现象是由字体本身来决定的,每一个字体都有自己的度量标准,在我们将字体设置为100px时,它的工作原理是这样的:
- 对字体定义一个
EM Square
,并按照字体的类型将其设置为1000或者1024等数值的基本单位,这是一个相对单位。 - 根据这个相对单位,设置好各项度量属性(如
ascender
,descender
,capital height
,x-height
等),而这些度量项有一部分是可以超出EM Square
的(如ascender
) - 在浏览器中,根据设置的字体大小,结合相对单位做对应的缩放,再将效果渲染在浏览器上
由于每一种字体的度量是不一样的,最终其表现在视觉上或者文字高度上效果就是不一样的。以Catamaran字体
为例,我们可以借助FontForge (opens new window)来获取其各项度量参数。如下图:
从上图可以得到Catamaran
的各种度量参数如下:
EM Size
(EM Square
)是1000个基本单位ascender
为1100单位,descender
是540单位。经过测试Mac下的浏览器似乎使用了HHead Ascent/Descent
,而Windows下使用Win Ascent/Descent
x-height
是485单位capital-height
是680单位
也就是说,Catamaran在1000个基本单位的EM-Square
中使用了1100+540=1640个基本单位来渲染字体,当设置font-size:100px
时,其真实的高度为1640/1000*100px=164px
。这个计算出来的高度就定义了元素的内容区域真实的高度,也就是我们在CSS中提到的行内格式化上下文(Inline formatting context)
(opens new window)中的content-area
。在本文中例子中,span
元素的深色背景区域即为content-area
。
从度量参数还可以得出,在font-size:100
下的Catamaran字体中,大写字母的高度(capital-height
)是68px(680单位),小写字母的高度(x-height
)为49px(485单位)。也就是说,在CSS中,1ex=49px,1em=100px。(后文会讲解相关CSS单位)。
在Catamaran字体下,设置font-size:100px
后各种高度示意如下:
2.3 line-height
行高是指内容区与以内容区为基础上下对称拓展的空白区域组成的区域高度。现在普遍的认为行高是相邻两行文本基线之间的距离。上文讨论了字体的度量属性时提到了不同的字体同样的字体大小有不一样的content-area
,即有不一样的内容高度(下文称content-area height
)。这个高度是由字体自身的度量属性来决定的。但是除了content-area height
以外,所有的行内元素还具有另外一个我们看不见的区域(下图称virtual-area
)的高度,这个高度就是行高,CSS对应的属性是line-height
。这两个高度是两个概念,其中line-height
被用来计算每一个行内框的高度,对应的是:
line-height
与content-area height
的高度差称为行距(leading),行距将被等分为两份分别添加在content-area
的顶部和底部。所以在视觉上,文字总是在由行高撑起的“盒子”中是居中的。在实际的场景中,这两个高度是可以相等的,也可以其中一个大于另外一个。当content-area height > line-height
时,就会出现leading为负的情况。特别的是,有几类元素的高度是由其height
,margin
,border
等属性来决定的。当设置height:auto
时,其line-height
和content-area height
是严格相等的。这些元素有:
- 替换元素,如
img
,input
,textarea
,svg
等 - display设置为
inline-block
或者其他inline-*
的元素 - 特定格式化上下文的元素,如flexbox中的子元素
在《寻根问底之line-height与vertical-align》这篇文章讨论line-height
时有这样一段描述:
浏览器默认是line-height:normal
,这个值具体是多少与字体本身有很大的关系,且不同浏览器也有差异,但其最终都会转为纯数字计算”。
那这个normal值在特定的字体下到底是多少呢?答案其实就藏在我们上文讨论的字体度量与context-area
里了。接下来以Catamaran字体为例做个简单的计算。
- EM-Square: Catamaran的
EM-Square
是1000个基本单位 - generals Ascent/Descent: 其Ascender和Descender分别是770个单位和230个单位,这个区域用于文字绘制(Typo Ascent/Descent)
- metrics Ascent/Descent: 其Asender和Descender分别是1100和540个单位。这个用于
content-area height
的计算,即1640个单位(HHead Asent/Descent) - metric Line Gap:通过将此值添加到metrics Ascent/Descent参与
line-height:normal
的计算(HHead Line Gap)
在Catamaran的字体中,line-gap为0,那么此时line-height:normal
将等于content-area height
,即1640单位,即1.64(相对于EM-Square)
而Arial字体就不一样了,如下图是Arial字体的度量参数:
可以看到,对于Arial字体来讲,其各项度量参数如下:
- EM-Square:
EM-Square
是2048个基本单位 - generals Ascent/Descent: Ascender和Descender分别是1491个单位和431个单位
- metrics Ascent/Descent: Asender和Descender费别是1854和434个单位,即
content-area height
为2287个单位 - metric Line Gap:67个单位
因此,100px的Arial字体,其content-area height=(1854+434)/2048*100=112px
,line-height=(1854+434+67)/2048*100=115px
,即line-height:normal
的值为1.15
。
从上面的一些结论可以得知,由于line-height
值类型为数字时是相对于font-size
来计算的,不是相对于content-area
,因此使用line-height:1
是一种不好的实践。因为它通常会导致line-height
计算的结果小于content-area height
。
2.4 vertical-align
在CSS中,line-height
是影响line box
高度计算的重要因素,而vertical-align
也是决定line-box
高度的一个主导因素之一。vertical-align
的默认值是baseline
。通常情况下,一个line-box
里面的文字会设置一致的font-size
和line-height
,这种情况下line-box
的高度一般会与line-height
保持一致。如相同font-size:100px
,line-height:120px
下的两个span
元素,效果如下:
两个元素非常整齐的排列到一起,此时line-box
的高度与line-height
一致。但是,假如把两个span元素设置为不同的字体大小,如将第二个的字体大小设置为前一个的一半,那么最终的效果是这样的:
可以发现,这种情况下line-box
的高度比line-height
要大。这是因为两个不同文字大小的span具有一样的line-height
,为了使它们保持在baseline
上对齐,就会造成一定的错位。而line-box
的高度是从子节点的最低点到最高点的距离来计算的,就导致了line-box
的高度比line-height
要大的现象。如果把line-height
的值设置为纯数字1.2而不是120px,此时字体较小的那个span元素计算出来的line-height
值为60px,其最低点会比另一个span的最低点高,此时line-box
的高度将会与line-height
相等。这就是为什么建议line-height
的值使用纯数字的其中一个原因。
除此之外,单个的子元素可能也会导致line-box
与line-height
不一致的现象。如下代码:
<p>
<span>Ba</span>
</p>
2
3
p {
line-height: 200px;
}
span {
font-family: Catamaran;
font-size: 100px;
}
2
3
4
5
6
7
这种代码给我们的直觉是最终line-box
的高度会是200px。但实际可能是大于200px的,其渲染后的结果是可能如下左图:
原因是每一个line-box都是由一个零宽字符开始的(如上图右图),这是一个看不见的字符(在《寻根问底之line-height与vertical-align》称为看不见的匿名节点)。外层的p标签本身有自己的字体,比如(serif
),即匿名节点使用的就是这个字体。而span标签的字体是Catamaran
。两个不同的字体其基线所在的位置可能是不同的。但是由于它们的line-height
都是200px,为了保证基线对齐,就会产生同上一个例子中相似的结果。
vertical-align
属性中,不仅仅是baseline
这个默认属性会有不如我们预期的表现。其实像middle
、top
等属性也是一样的道理。按照规范,middle对齐指的是“元素中部(content-area中心)与父元素基线往上二分之一x-height
高度处对齐”。同样由于不同的字体有不同的基线比、x-height等,一般我们视觉上的垂直居中对齐实际上只是近似的垂直居中。除此之外,vertical-align还有以下几个属性,其对齐的形式如下图所示:
top/bottom
: 元素的顶部/底部与line-box
的顶部/底部对齐text-top/text-bottom
:元素的顶部/底部与content-area
顶部/底部对齐
以上四个属性值都是与元素的顶部或底部相关。这个顶部或底部是包含了元素的行高在内。也就是说元素的line-height
是会影响对齐的视觉效果的,如下图:
上图中,白色背景的字符由于设置了较大的line-height
,因此,当设置vertical-align:top
时,其顶端是virtual-area
的顶端,对齐时就会导致视觉上不如预期效果的现象。
除关键词外,vertical-align
的值类型是支持数字以及百分比的,这在解决一些对齐的问题时显得尤为重要。
2.5 应用
虽然已经了解了字体特性、度量属性以及line-height
和vertical-align
共同作用于的效果,但是目前为止,针对字体的本身的特性,我们几乎是没有办法使用CSS来控制字体的度量属性的。好在同一字体的度量属性都是个常量,因此我们可以借助这些属性,通过一些简单的计算达到我们一些排版的需求。比如以下例子中以Catamaran字体为例实现了一个大写字母的高度为100px以及实现了真正意义上的垂直居中。
由上文知道,Catamaran字体的度量属性如下:
- EM-Square:
EM-Square
是1000个基本单位 - metrics Ascent/Descent: 其Asender和Descender费别是1100和540个单位
- metric Line Gap:0
- x-height是485单位
- capital-height是680单位
做如下计算:
p {
/* font metrics */
--font: Catamaran;
--fm-capitalHeight: 0.68;
--fm-descender: 0.54;
--fm-ascender: 1.1;
--fm-linegap: 0;
/* 期望的大小字母高度 */
--capital-height: 100;
font-family: var(--font);
/* 计算出对应大小字母高度下的font-size值 */
--computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
font-size: calc(var(--computedFontSize) * 1px);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
效果如下:
此时已经实现了大小字母的高度为100px的效果,但是我们的期望是大写字母能够在可视区域内是垂直居中的,也就是空白的空间能够平分于文本的上下。而实际上受asender/desender
比例的影响,并不是完美垂直居中的,这就需要额外的利用这些属性来计算一个合适的vertical-align
值。
首先,计算出line-height:normal
和content-area
的高度:
p {
/* ... */
--lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));
--contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}
2
3
4
5
接着计算出大小字母顶部到上边缘的距离以及底部到下边缘的距离:
p {
/* … */
--distanceBottom: (var(--fm-descender));
--distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
}
2
3
4
5
这两个值的差值就是文本相对于绝对的中点的偏移值,结合当前字体的大小,可以计算出这个绝对的值。并将这个值应用在子元素span
上。
p {
/* ... */
--valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
vertical-align: calc(var(--valign) * -1px);
}
2
3
4
5
6
7
最后,计算line-height
的值。
p {
/* … */
/* 期望的 line-height */
--line-height: 3;
line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
}
2
3
4
5
6
对文本设置不同的line-height,效果如下:
到此我们已经严格实现了让大写字母大小设定为指定的值,并严格的垂直居中。
# 三、CSS中的文字单位
在CSS中,文字单位有很多,最常见的有px
、em
、rem
。本文将主要介绍除px
外的几个单位。
# 1. em
em
是一个相对单位。在上文中我们介绍了EM Square
,同时知道了当我们给字体设置font-size:100px
时,文字占据的实际高度可能是大于100px的,如Catamaran字体是164px。这就导致了不同的字体同样的字体大小有不同的content-area height
。好在em
这个单位不是相对于content-area height
来计算的,而是相对于font-size
本身。
<body>
<div>Demo</div>
</body>
2
3
body {
font-size: 14px;
}
div {
font-size: 1.2em; /* 14px * 1.2 = 16.8px*/
}
2
3
4
5
6
em
这个单位是有一定的缺陷的,主要体现在标签嵌套时line-height
等属性的继承上。
(1)font-size继承
<body>
<div>
level 1 <!-- 14px * 1.2 = 16.8px -->
<div>
level 2 <!-- 16.8px * 1.2 = 20.16px -->
<div>
level 3 <!-- 20.16px * 1.2 = 24.192px -->
</div>
</div>
</div>
</body>
2
3
4
5
6
7
8
9
10
11
(2)line-height继承
<div class="parent">
Parent
<div class="child red">
Child
</div>
<div class="child child2">
Child
</div>
</div>
2
3
4
5
6
7
8
9
.parent {
font-size: 14px;
line-height: 1.5em;
}
.child {
font-size: 60px;
}
.red {
color: red;
}
2
3
4
5
6
7
8
9
10
效果如下:
父元素的line-height:1.5em
计算后得到的21px的行高会原封不动的继承给子元素,由于子元素的content-area height
大于21px,所以就导致前后的line-box
重叠在了一起。因此,给line-height
属性设置值时,通常会设置为纯数字(如1.5)来代替em
,除非你清楚的知道使用em
后可能引起的问题并保证不会引起此类问题。
# 2. rem
出于一些响应式布局的需要,以及解决上述em
单位的缺陷,就诞生了rem
这个单位。这里的“r”是“root”的意思。其含义是相对于文档根元素文字大小来计算的单位。这个根元素通常是只html
标签。
html {
font-size: 100px;
}
div {
font-size: .14rem; /* 100px * 0.14 = 14px */
}
2
3
4
5
6
相比于em
,rem
就不存在标签嵌套时继承的问题,因为其计算的相对值永远都是相对于根元素的来计算的。因为这个特性的存在,rem
单位在vh
和vw
单位还未诞生之前,常被用来作为响应式布局的一种的有效的手段。
@media screen and (max-width: 359px) and (min-width: 320px) {
html, body {
font-size: 50px !important;
}
}
@media screen and (max-width: 374px) and (min-width: 360px) {
html, body {
font-size: 56.25px !important;
}
}
2
3
4
5
6
7
8
9
10
# 3. ex
ex
的大小是小写字母x的高度,即上文中提到的x-height
。这是一个非常生僻的单位,实践上很少会使用到,主要原因是不同字体的x-height
值是不一样的。然而该单位在很古老的浏览器甚至都是支持的,本着存在即合理的原则,ex
这个单位也并不是毫无价值的。如果我们想实现一些效果不受字体的因素影响的时,那么ex
就派上用场了。具体的示例,可以参考张鑫旭的文章《字母’x’在CSS世界中的角色和故事》 (opens new window)
# 4. ch
ch
也是一个鲜为人知的单位,它的大小等于数字0
的宽度,它也是一个相对单位,会根据字体的大小计算。如5ch
就是指5个字符0
的宽度。同时对于ch
有一个计算规则,就是1ch=1英文=1数字。由于只是对英文和数字做了定义,因此,当使用的字符集为非英文时,ch
单位并不是代表字符的个数。另外,通过实践发现,其实计算的规则仅仅对于等宽字体起作用。比如5ch
的容器,有时可以容下超过5个的英文字母(如i和l等字母)。正因为ch
单位的这些特性,实际中一般仅在等宽字体中来使用(如代码编辑器、命令行窗口里的代码)。指定多少个字符数,我们更多的会采用em
这个稳定的单位。
# 四、总结
本文重点
- 了解字体的基本属性
- 理解文字的度量属性(
font metrics
) - 掌握
content-area
的定义 - 掌握
line-height
的定义以及line-height:normal
的计算规则 - 掌握
vertical-align
各种属性值的表现 - 掌握
line box
高度的计算规则 - 掌握常见的CSS文字单位