line-height 属相详解

写在前面

本文摘自 «CSS 世界»(附加部分个人见解)

内联元素

理解 line-height 和 vertial-align 之前需要了解下内联元素。

哪些元素是内联元素

  1. 从定义看
    首先要明白这一点:“内联元素”的“内联”特指“外在盒子”,和“display 为 inline的元素”不是一个概念!inline-block 和 inline-table 都是“内联元素”,因为它们的“外在盒子”都是内联盒子。自然 display:inline 的元素也是“内联元素”,那么,<button>按钮元素是内联元素,因为其 display 默认值是 inline-block;<img>图片元素也是内联 display 默认值是 inline 等。
  2. 从表现看
    就行为表现来看,“内联元素”的典型特征就是可以和文字在一行显示。因此,文字是内联元素,图片是内联元素,按钮是内联元素,输入框、下拉框等原生表单控件也是内联元素。下面有一个疑问:浮动元素貌似也是可以和文字在一个水平上显示的,是不是浮动元素也是内联级别的呢?不是的。实际上,浮动元素和后面的文字并不在一行显示,浮动元素已经在文档流之外了。证据就是,当后面文字足够多的时候,文字并不是在浮动元素的下面,而是继续在后面。这就说明,浮动元素和后面文字不在一行,只是它们恰好站在了一起而已。真相是,浮动元素会生成“块盒子”,这就是后话了。

内联世界深入的基础—内联盒模型

下面是一段很普通的 HTML:

1
<p>这是一行普通的文字,这里有个 <em>em</em> 标签。</p>

看似普通,实际上包含了很多术语和概念,或者换种通俗的说法,包含了很多种盒子。我归结为下面这些盒子。
(1)内容区域(content area)。内容区域指一种围绕文字看不见的盒子,其大小仅受字符本身特性控制,本质上是一个字符盒子(character box);但是有些元素,如图片这样的替换元素,其内容显然不是文字,不存在字符盒子之类的,因此,对于这些元素,内容区域可以看成元素自身。定义上说内容区域是“看不见的”,这对理解“内容区域”是不利的,好在根据我多年的理解与实践,我们可以把文本选中的背景色区域作为内容区域,例如,如下图所示。
内容区域示意
  这对于解释各种内联相关的行为都非常可行,文本选中区本质上就等同于基本盒尺寸中的content box,都是 content,语义上也说得通。实际上,内容区域并没有明确的定义,所以将其理解为 em 盒(em-box,可看成是中文字符占据的 1 em 高度区域)也是可以的,但是在本文中,为了方便演示和讲解,将其全部理解为文本选中的区域。
  在 IE 和 Firefox 浏览器下,文字的选中背景总能准确反映内容区域范围,但是 Chrome 浏览器下,::selection 范围并不总是准确的,例如,和图片混排或者有垂直 padding 的时候,范围会明显过大,这一点需要注意。后面行高等章节会利用此选中背景帮助我们理解。

(2)内联盒子(inline box)。“内联盒子”不会让内容成块显示,而是排成一行,这里的“内联盒子”实际指的就是元素的“外在盒子”,用来决定元素是内联还是块级。该盒子又可以细分为“内联盒子”和“匿名内联盒子”两类:
内联盒子示意
  如果外部含内联标签(<span><a><em>等),则属于“内联盒子”(实线框标注);如果是个光秃秃的文字,则属于“匿名内联盒子”(虚线框标注)。需要注意的是,并不是所有光秃秃的文字都是“匿名内联盒子”,其还有可能是“匿名块级盒子”,关键要看前后的标签是内联还是块级。
(3)行框盒子(line box)。例如:
行框盒子示意
每一行就是一个“行框盒子”(实线框标注),每个“行框盒子”又是由一个一个“内联盒子”组成的。
(4)包含盒子(containing box)。例如:
包含盒子示意
<p>标签就是一个“包含盒子”(实线框标注),此盒子由一行一行的“行框盒子”组成。

幽灵空白节点

“幽灵空白节点”是内联盒模型中非常重要的一个概念,具体指的是:在 HTML5 文档声明中,内联元素的所有解析和渲染表现就如同每个行框盒子的前面有一个“空白节点”一样。这个“空白节点”永远透明,不占据任何宽度,看不见也无法通过脚本获取,就好像幽灵一样,但又确确实实地存在,表现如同文本节点一样,因此,我称之为“幽灵空白节点”。注意,这里有一个前提,文档声明必须是 HTML5 文档声明(HTML 代码如下),如果还是很多年前的老声明,则不存在“幽灵空白节点”。我们可以举一个最简单的例子证明“幽灵空白节点”确实存在, CSS 和 HTML 代码如下:

1
2
3
4
5
6
7
div {
background-color: #cd0000;
}
span {
display: inline-block;
}
<div><span></span></div>

结果,此<div>的高度并不是 0,而是如图所示有高度。这着实很奇怪,内部的<span>元素的宽高明明都是 0,标签之间也没有换行符之类的嫌疑,怎么<div>的高度会是图3-29 中所示的 18 像素呢?
幽灵空白节点示意
当然,为何高度是 18 像素这里三言两语是解释不清的,可以看后面对 line-height 和vertical-align 的深入讲解,这里只是为了证明“幽灵空白节点”确实是存在的。虽然说“幽灵空白节点”是我自己根据 CSS 的特性表现起的一个非常形象的名字,但其绝不是空中楼阁、信口胡诌的。规范中实际上对这个“幽灵空白节点”是有提及的,“幽灵空白节点”实际上也是一个盒子,不过是个假想盒,名叫“strut”,中文直译为“支柱”,是一个存在于每个“行框盒子”前面,同时具有该元素的字体和行高属性的 0 宽度的内联盒。

内联元素的高度之本 line-height

先思考下面这个问题:默认空<div>高度是 0,但是一旦里面写上几个文字,<div>高度就有了,请问这个高度由何而来,或者说是由哪个 CSS 属性决定的?如果仅仅通过表象来确认,估计不少人会认为<div>高度是由里面的文字撑开的,也就是font-size 决定的,但本质上是是由 line-height 属性全权决定的,尽管某些场景确实与font-size 大小有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="test1">我的高度是?</div>
.test1 {
font-size: 16px;
line-height: 0;
border: 1px solid #ccc;
background: #eee;
}

<div class="test2">我的高度是?</div>
.test1 {
font-size: 0;
line-height: 16px;
border: 1px solid #ccc;
background: #eee;
}

这两段代码的区别在于一个 line-height 行高为 0,一个 font-size 字号为 0。结果,第一段代码,最后元素的高度只剩下边框那么丁点儿,而后面一段代码,虽然文字小到都看不见了,但是 16px 的内部高度依然坚挺,如图所示。很显然,从上面这个例子可以看出,<div>高度是由行
高决定的,而非文字。
文字高度本质上由行高决定
  下面要说一些很有意思的结论,对于非替换元素的纯内联元素,其可视高度完全由 line-height 决定。注意这里的措辞—“完全”,什么padding、border 属性对可视高度是没有任何影响的,这也是我们平常口中的“盒模型”约定俗成说的是块级元素的原因。
  因此,对于文本这样的纯内联元素,line-height 就是高度计算的基石,用专业说法就是指定了用来计算行框盒子高度的基础高度。比方说,line-height 设为16px,则一行文字高度是 16px,两行就是 32px,三行就是 48px,所有浏览器渲染解析都是这个值,1 像素都不差。
  那如果是替换元素,又或者是块级元素,line-height 在其中又扮演什么角色呢?
  在回答这个问题之前,我们最好先把 line-height 作用于内联元素的细节给搞明白。通常,line-height 的高度作用细节都是使用“行距”和“半行距”来解释的。那么什么是“行距”,什么又是“半行距”呢?
  我个人是这么认为的:内联元素的高度由固定高度和不固定高度组成,这个不固定的部分就是这里的“行距”。换句话说,line-height 之所以起作用,就是通过改变“行距”来实现的。
  古人的印刷作品水平方向但是,水平方向,列与列之间却有着明显的间隙,这个间隙其实就是“行距”。所以,“行距”的作用是可以瞬间明确我们的阅读方向,让我们阅读文字更轻松。在 CSS 世界中,“行距”其实也是类似的东西,但还是有些差别的。以水平阅读流举例,传统印刷的“行距”是上下两行文字之间预留的间隙,是个独立的区域,也就意味着第一行文字的上方是没有“行距”的;但是在 CSS 中,“行距”分散在当前文字的上方和下方,也就是即使是第一行文字,其上方也是有“行距”的,只不过这个“行距”的高度仅仅是完整“行距”高度的一半,因此,也被称为“半行距”。
  现在知道了 CSS 的“半行距”,那么哪里到哪里才是“半行距”的高度范围呢?一般业界的共识是:行距 = 行高− em-box。转换成 CSS 语言就是:行距 = line-height - font-size。其中 em-box 是 CSS 世界中比较虚的一个概念,说“虚”并不是胡编乱造的意思,而是我们无法有效感知这个盒子具体的位置在哪里,但是有一点可以明确,就是其高度正好就是 1em。em是一个相对 font-size 大小的 CSS 单位,因此 1em 等用于当前一个 font-size 大小,这就是“行距 = line-height - font-size”这个公式的由来。有了“行距”,我们一分为二,就有了“半行距”,分别加在 em-box 上面和下面就构成了文字的完整高度了。话虽这么讲,但一旦不弄清楚 em-box 究竟在什么位置,我们就无法在脑中形成关于行高的具象认知,知识很容易遗忘。
  人很容易被肉眼所见的东西迷惑,因此,很多人会把文字图形区域看成是 em-box 范围,实际上这是不正确的,比方说,一些带尾的英文字符 q 或者 g,其小尾巴是在 em-box 范围之外的,而对于汉字,很多字体图形高度实际上要小于 em-box 高度的。
  此时,就轮到内容区域(content area)出马了。在本书中,内容区域可以近似理解为 Firefox/IE浏览器下文本选中带背景色的区域。这么理解的重要原因之一就是可见,这对于我们深入理解内联元素知识非常有帮助。
  大多数场景下,内容区域和 em-box 是不一样的,内容区域高度受 font-family 和font-size 双重影响,而 em-box 仅受 font-size 影响,通常内容区域高度要更高一些。除了下面这种情况,也就是“当我们的字体是宋体的时候,内容区域和 em-box 是等同的”,因为宋体是一种正统的印刷字体,方方正正,所以千万不要小看宋体。
  于是,利用我们平常不待见的宋体,就能准确揪出“半行距”的藏身之所了,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
.test {
font-family: simsun;
font-size: 24px;
line-height: 36px;
background-color: yellow;
}
.test > span {
background-color: white;
}
<div class="test">
<span>sphinx</span>
</div>

此时,平常虚无的 em-box 借助内容区域(图 5-8 中字符 sp 的选中区域)暴露出了庐山真面目,“半行距”也准确显现出来了
半行间距

  说完了内联元素,下面轮到替换元素和块级元素了。
  关于替换元素的高度与 line-height 的关系首先需要弄明白这个问题:line-height可以影响替换元素(如图片的高度)吗?答案是,不可以!
  可能有人会反驳了,不会呀,你看下面这个例子:

1
2
3
4
5
.box {
line-height: 256px;
}
<div class="box">
<img src="1.jp

  不是的,不是 line-height 把图片占据高度变高了,而是把“幽灵空白节点”的高度变高了。图片为内联元素,会构成一个“行框盒子”,而在 HTML5 文档模式下,每一个“7行框盒子”的前面都有一个宽度为 0 的“幽灵空白节点”,其内联特性表现和普通字符一模一样,所以,这里的容器高度会等于 line-height 设置的属性值 256px。
  实际开发的时候,图文和文字混在一起是很常见的,那这种内联替换元素和内联非替换元素在一起时的高度表现又是怎样的呢?
  由于同属内联元素,因此,会共同形成一个“行框盒子”,line-height 在这个混合元素的“行框盒子”中扮演的角色是决定这个行盒的最小高度,听上去似乎有点儿尴尬,对于纯文本元素,line-height 非常威风,直接决定了最终的高度。但是,如果同时有替换元素,则line-height 的表现一下子弱了很多,只能决定最小高度,对最终的高度表现有望尘莫及之感。为什么会这样呢?一是替换元素的高度不受 line-height 影响,二是 vertical-align属性在背后作祟。对于这种混合替换元素的场景,line-height 要想一统江山,需要值足够大才行。但是,实际开发的时候,我们给 line-height 设置的值总是很中规中矩,于是,就会出现类似下面的场景:明明文字设置了 line-height 为 20px,但是,如果文字后面有小图标,最后“行框盒子”高度却是 21px 或是 22px。这种现象背后最大的黑手其实是 vertical-align 属性,我们会在下一章好好深入剖析为什么会有这样的表现。
  对于块级元素,line-height 对其本身是没有任何作用的,我们平时改变 line-height,块级元素的高度跟着变化实际上是通过改变块级元素里面内联级别元素占据的高度实现的。

为什么 line-height 可以让内联元素“垂直居中”

坊间流传着这么一种说法:“要想让单行文字垂直居中,只要设置 line-height 大小和height 高度一样就可以了。从效果上看,似乎验证了这种说法的正确性。但是,实际上,上面的说法对 CSS 初学者会产生两个严重的误导,同时,语句本身也存在不严谨的地方!
  误区之一:要让单行文字垂直居中,只需要 line-height 这一个属性就可以,与 height
一点儿关系都没有。也就是说,我们直接入下图写法就行了。

1
2
3
.title {
line-height: 24px;
}

  误区二:行高控制文字垂直居中,不仅适用于单行,多行也是可以的。准确的说法应该是“line-height 可以让单行或多行元素近似垂直居中”。稍等,这里有个词似乎和上面的表述有点儿微妙的差异,“近似垂直居中”?没错,一定要加上“近似”二字,这样的说法才足够严谨。换句话说,我们通过 line-height 设置的垂直居中,并不是真正意义上的垂直居中!究竟是怎么一回事?
  这里,其实要解答的是两个问题,一个是为何可以“垂直居中”,另一个是为何是“近似”。正如上一节所说的,没有什么理所当然,行高可以实现“垂直居中”原因在于 CSS 中“行距的上下等分机制”,如果行距的添加规则是在文字的上方或者下方,则行高是无法让文字垂直居中的。
说“近似”是因为文字字形的垂直中线位置普遍要比真正的“行框盒子”的垂直中线位置低,譬如我们拿现在用得比较多的微软雅黑字体举例:

1
2
3
4
5
6
7
8
p {
font-size: 80px;
line-height: 120px;
background-color: #666;
font-family: 'microsoft yahei';
color: #fff;
}
<p>微软雅黑</p>

结果,我都不需要标注,肉眼就能看出字形明显偏下:
line-height 与位置下沉的微软雅黑字体
由于我们平时使用的 font-size 都比较小,12px~16px 很多,因此,虽然微软雅黑字体有下沉,但也就 1 像素的样子,所以我们往往觉察不到这种“垂直对齐”其实并不是真正意义上的垂直居中,只是感官上看上去像是垂直居中罢了。这也是我总是称line-height 实现的单行文本垂直居中为“近似垂直居中”的原因。
  下面再来说说行高实现多行文本或者图片等替换元素的垂直居中效果实现。多行文本或者替换元素的垂直居中实现原理和单行文本就不一样了,需要 line-height属性的好朋友 vertical-align 属性帮助才可以,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.box {
line-height: 120px;
background-color: #f0f3f9;
width: 300px;
}
.content {
display: inline-block;
line-height: 20px;
margin: 0 20px;
vertical-align: middle;
}
<div class="box">
<div class="content">基于行高实现的基于行高实现的基于行高实现的基于行高实现的...</div>
</div>

line-height 与多行文字垂直居中效果
  实现的原理大致如下。
  (1)多行文字使用一个标签包裹,然后设置 display 为 inline-block。好处在于既能重置外部的 line-height 为正常的大小,又能保持内联元素特性,从而可以设置vertical-align 属性,以及产生一个非常关键的“行框盒子”。我们需要的其实并不是这个“行框盒子”,而是每个“行框盒子”都会附带的一个产物—“幽灵空白节点”,即一个宽度为0、表现如同普通字符的看不见的“节点”。有了这个“幽灵空白节点”,我们的 lineheight:120px就有了作用的对象,从而相当于在.content 元素前面撑起了一个高度为120px 的宽度为 0 的内联元素。
  (2)因为内联元素默认都是基线对齐的,所以我们通过对.content 元素设置 verticalalign:middle来调整多行文本的垂直位置,从而实现我们想要的“垂直居中”效果。如果是要借助 line-height 实现图片垂直居中效果,也是类似的原理和做法。

这里解释下:
(1)第一点,为什么要设置成 inline-block 如果 .content 不设置成 inline-block,就不会构成行框盒子了,而外层box本身是块元素,line-height 对它无用,所以整个 div 的高度取决于 .conent里面文字的 line-height了, 如下图:
行高

这里还举另外要给例子:

1
2
3
4
5
6
7
8
9
10
11
.box {
line-height: 120px;
background-color: #f0f3f9;
width: 300px;
}
.content {
line-height: 20px;
}
<div class="box">
<span class="content">基于行高实现的基于行高实现的基于行高实现的基于行高实现的...</span>
</div>

行高
为什么 span 的行高没有生效? 而 .content 设置成 inline-block 就生效了呢?
因为同样这里形成了一个行框盒子,span 的高度实际取决于最小行高,而最小行高就是幽灵空白节点(strut)的行高。而改成 inline-block ,就会创建一个独立的“行框盒子”,这样<span>元素设置的line-height:20px 就可以生效了。或者改成 block

这里还引用书中的另一个例子,书中解释的更加详细一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.box {
line-height: 96px;
}
.box span {
line-height: 20px;
}
<!--和-->
.box {
line-height: 20px;
}
.box span {
line-height: 96px;
}
<div class="box">
<span>内容...</span>
</div>

.box 实际高度都为96px,为什么?
也就是说:无论内联元素 line-height 如何设置,最终父级元素的高度都是由数值大的那个 line-height 决定的,我称之为“内联元素 line-height 的大值特性”。
首先,要明确一点:内联元素是支持 line-height 的<span>元素上的 line-height 也确实覆盖了.box 元素,但是,在内联盒模型中,存在一些你看不到的东西,没错,就是多次提到的“幽灵空白节点”。
。这里的<span>是一个内联元素,因此自身是一个“内联盒子”,本例就这一个“内联盒子”,只要有“内联盒子”在,就
一定会有“行框盒子”,就是每一行内联元素外面包裹的一层看不见的盒子。然后,重点来了,在每个“行框盒子”前面有一个宽度为 0 的具有该元素的字体和行高属性的看不见的“幽灵空白节点”,如果套用本案,则这个“幽灵空白节点”就在<span>元素的前方,如图所示。
行高
于是,就效果而言,我们的 HTML 实际上等同于:

1
2
3
<div class="box">
字符<span>内容...</span>
</div>

这下就好理解了,当.box 元素设置 line-height:96px 时,“字符”高度 96px;当设置 line-height:20px 时,<span>元素的高度则变成了 96px,而行框盒子的高度是由高度最高的那个“内联盒子”决定的,这就是.box 元素高度永远都是最大的那个 line-height的原因。
知道了原因也就能够对症下药,要避免“幽灵空白节点”的干扰,例如,设置<span>元素 display:inline-block,创建一个独立的“行框盒子”,这样<span>元素设置的line-height:20px 就可以生效了,这也是多行文字垂直居中示例中这么设置的原因。

(2)对于第二点,在设置 vertail-align 之前,.content 元素的基线默认是与该元素所在行的基线对齐的,如下图:
行高
而该元素所在行的基线在哪儿呢?这个基线就取决于那个幽灵空白节点的基线,但是.content 的基线在哪儿了呢,这个问题也就是问 inline-block 元素的基线在哪儿,这里我们来试验一下:
行高
这里可以明显的看出,inline-block 元素的基线就是其内部的最后一行行内元素的基线所在的位置。所以 .content 的基线就与幽灵空白节点的基线对齐了,然后用 vertail-align: middle 就可以使之居中对齐了。