大家熟知的Line-height
和vertical-align
是简单的CSS属性。如此简单,以至于我们大多数人都确信完全理解它们的工作原理和使用方法。真的是这样么,我觉得事实并非如此。它们确实很复杂,也是CSS中难点之一,因为它们在创建 CSS 鲜为人知的各种功能方面发挥着重要作用:内联格式化上下文。
例如,line-height
可以设置为长度或无单位值,但默认为normal
. 好的,但什么是正常?我们经常读到它是(或应该是)1,或者可能是 1.2,甚至CSS 规范在这一点上也不清楚。我们知道无单位line-height
是font-size
相对的,但问题是它在不同字体系列中表现不同,所以总是相同还是不同?真的在1到1.2之间吗?并且,它的含义是什么?font-size: 100px``line-height``vertical-align``line-height
深入研究一个不那么简单的 CSS 机制……
我们font-size
先来说说
看看这个简单的 HTML 代码,一个<p>
包含 3 的标签<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
的字体与不同的字体系列一起使用会产生具有不同高度的元素:
*
即使我们知道这种行为,为什么不创建 100px 高度的元素呢?我测量并发现了这些值:Helvetica:115px,Gruppo:97px 和 Catamaran:164pxfont-size: 100px
*
虽然一开始看起来有点奇怪,但这是完全可以预料的。**原因在于字体本身**。下面是它的工作原理:
- 一种字体定义了它的em-square(或 UPM,units per em),一种将在其中绘制每个字符的容器。这个平方使用相对单位,一般设置为1000个单位。但它也可以是 1024、2048 或其他任何值。
- 根据其相对单位,设置字体规格(升序、降序、大写高度、x 高度等)。请注意,某些值可能会溢出到 em-square 之外。
- 在浏览器中,相对单位被缩放以适合所需的字体大小。
让我们使用 Catamaran 字体并在FontForge中打开它以获取指标:
- em-square 是 1000
- ascender 是 1100,descender 是 540。运行一些测试后,浏览器似乎在 Mac OS 上使用HHead Ascent / Descent值,在 Windows 上使用 Win Ascent / Descent值(这些值可能不同!)。我们还注意到资本高度为 680,X 高度为 485。
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/38f8e5245f2d42b0b0cf5824dc0dc331~tplv-k3u1fbpfcp-zoom-1.image)
*
这意味着 Catamaran 字体在 1000 个单位的 em-square 中使用 1100 + 540 个单位,这在设置时给出 164px 的高度。**这个计算出的高度定义了** **元素的** ***内容区域***,我将在本文的其余部分引用这个术语。您可以将*内容区域*视为属性适用的区域[2](https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align#fn-2 "这不是严格意义上的")。`font-size: 100px`****** **`background`[](https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align#fn-2 "这不是严格意义上的")
我们还可以预测大写字母高 68px(680 个单位),小写字母(x 高度)高 49px(485 个单位)。因此,1ex
= 49px 和1em
= 100px,而不是 164px(谢天谢地,em
是基于font-size
,而不是计算的高度)
*
在深入探讨之前,请简要说明它涉及的内容。当一个<p>
元素在屏幕上呈现时,它可以根据其宽度由许多行组成。每行由一个或多个内联元素(HTML 标签或文本内容的匿名内联元素)组成,称为line-box。line-box 的高度基于它的孩子的高度**。因此,浏览器会计算每个内联元素的高度,从而计算行框的高度(从其子项的最高点到其子项的最低点)。因此,line-box总是足够高以包含其所有子项(默认情况下)。
每个 HTML 元素实际上是一堆line-boxes。如果你知道每个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>
它将生成 3 个行框:
- 第一个和最后一个都包含一个匿名内联元素(文本内容)
- 第二个包含两个匿名内联元素,第三个
<span>
*
<p>
(黑色边框)由包含内联元素(实线边框)和匿名内联元素(虚线边框)的行框(白色边框)组成我们清楚地看到第二个*线框*比其他线框高,这是因为计算了它的子项的*内容区域*,更具体地说,是使用 Catamaran 字体的那个。
line-box创建的难点在于我们不能真正看到,也不能用 CSS 控制它。即使应用背景也不会给我们任何关于第一个line-box高度的视觉线索。::first-line
**
line-height
: 问题及其他
到现在为止,我介绍了两个概念:content-area和line-box。如果你没看错的话,我说过line-box的高度是根据它的children的高度来计算的,我并没有说它的children content-area的高度。这有很大的不同。
尽管这听起来很奇怪,但内联元素有两个不同的高度:内容区域高度和虚拟区域高度(我发明了术语虚拟区域,因为高度对我们来说是不可见的,但你不会发现任何出现在规范中)。
- 内容区域高度由字体规格定义(如前所述)
- virtual-area height 是,
line-height
它是*用于计算line-box*高度的高度
*
话虽如此,它打破了`line-height`基线之间距离的普遍看法。在 CSS 中,它不是[3](https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align#fn-3 "在其他编辑软件中,它可能是基线之间的距离。 在 Word 或 Photoshop 中,就是这种情况。 主要区别在于第一行在 CSS 中也受到影响")。
*
*计算出的虚拟区域*和*内容区域*之间的高度差称为行距。*前导的一半添加到内容区域*的顶部,另一半添加到底部。**因此*content-area*总是在*virtual-area***的中间。
根据其计算值,line-height
( virtual-area ) 可以等于、高于或小于content-area。在较小的virtual-area的情况下,leading 是负的并且line-box在视觉上比它的孩子小。
还有其他类型的内联元素:
- 替换内联元素(
<img>
,<input>
,<svg>
等) inline-block
和所有inline-*
元素- 参与特定格式化上下文的内联元素(例如,在 flexbox 元素中,所有 flex 项目都是块化的)
对于这些特定的内联元素,高度是根据它们的height
,margin
和border
属性计算的。如果height
是auto
,则line-height
使用 并且内容区域严格等于line-height
.
*
无论如何,我们仍然面临的问题是`line-height`的`normal`价值是多少?至于*内容区域*高度的计算,答案可以在字体指标中找到。
那么让我们回到 FontForge。双体船的 em-square 是 1000,但我们看到许多上升/下降值:
- generals Ascent/Descent : ascender 为 770,descender 为 230。用于人物绘图。(表 “OS/2” )
- metrics Ascent/Descent:ascender 为 1100,descender 为 540。用于content-area*的高度。(表 *“hhea” 和表 “OS/2” )
- 公制线间隙。用于, 通过将此值添加到上升/下降*指标。(表 *“hhea” )
line-height: normal
****
在我们的例子中,Catamaran 字体定义了一个 0 单位的行间距,因此等于 content-area ,即 1640 个单位,或 1.64。line-height: normal
**
作为比较,Arial 字体描述了一个 2048 个单位的 em-square,一个 1854 个单位的 ascender,一个 434 个单位的下行和一个 67 的行间距。这意味着给出了112px(1117 个单位)的内容区域和115px的内容区域(1150 个单位或 1.15)。所有这些指标都是特定于字体的,由字体设计师设置。font-size: 100px
**line-height: normal
很明显,设置是一种不好的做法line-height: 1
。我提醒您,无单位值是font-size
相对的,而不是内容区域的相对值,处理小于内容区域的 虚拟区域是我们许多问题的根源。
*
但不仅如此。对于它的价值,在我的计算机上安装的 1117 种字体(是的,[我安装了所有来自 Google Web Fonts 的字体](https://github.com/qrpike/Web-Font-Load))中,1059 种字体(大约 95%)的计算值大于 1。它们的计算值从 0.618 到 3.378。你读得很好,3.378!`line-height: 1`[](https://github.com/qrpike/Web-Font-Load)`line-height``line-height`
line-box计算的小细节:
- 对于内联元素,
padding
增加border
背景区域,但不增加content-area的高度(也不增加line-box的高度)。因此,内容区域并不总是您在屏幕上看到的。margin-top
并且margin-bottom
没有效果。 - 对于替换的内联元素
inline-block
和块化的内联元素:padding
,margin
并border
增加height
,因此内容区域和行框的高度
vertical-align
: 一个属性来控制一切
我还没有提到这个vertical-align
属性,尽管它是计算line-box高度的一个重要因素。我们甚至可以说,vertical-align
内联格式化上下文可能具有主导作用。
默认值为baseline
。您是否提醒字体指标升序器和降序器?这些值决定了基线的位置,以及比率。由于上升器和下降器之间的比例很少是 50/50,它可能会产生意想不到的结果,例如兄弟元素。
从该代码开始:
<p>
<span>Ba</span>
<span>Ba</span>
</p>
p {
font-family: Catamaran;
font-size: 100px;
line-height: 200px;
}
具有 2 个兄弟姐妹的<p>
标签<span>
inheritingfont-family
和font-size
fixed line-height
。基线将匹配并且line-box的高度等于它们的line-height
。
*
如果第二个元素有一个更小的`font-size`怎么办?
span:last-child {
font-size: 50px;
}
听起来很奇怪,默认基线对齐可能会导致更高(!)的 line-box,如下图所示。我提醒你,一个line-box的高度是从它的孩子的最高点到它的孩子的最低点计算的。
*
这可能是支持使用[无单位值](http://allthingssmitty.com/2017/01/30/nope-nope-nope-line-height-is-unitless/)[的论据`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)。**老实说,无论你选择什么,你总是会遇到内联对齐的问题**。
看看这个另一个例子。带有的<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>
可能不同,因此行框的高度比预期的要高。发生这种情况是因为浏览器在进行计算时就好像每个行框都以零宽度字符开头,该规范称为 strut。
一个看不见的角色,但一个看得见的影响。
继续,我们面临着与兄弟元素相同的先前问题。
*
基线对齐被拧紧了,但是如何进行救援呢?正如您在规范中看到的那样,“将盒子的垂直中点与父盒子的基线加上父盒子 x 高度的一半对齐”。基线比率不同,x 高度比率也不同,因此 对齐也不可靠。最糟糕的是,在大多数情况下,永远不会真正“处于中间”。涉及的因素太多,无法通过 CSS 设置(x-height、ascender/descender ratio 等)vertical-align: middle``middle
middle
middle
作为旁注,还有 4 个其他值,在某些情况下可能有用:
vertical-align: top
/bottom
对齐到行框的顶部或底部vertical-align: text-top
/text-bottom
对齐到内容区域的顶部或底部
*
但要小心,在所有情况下,它都会对齐*virtual-area*,因此是不可见的高度。看看这个使用. **Invisible** **可能会产生奇怪但不足为奇的结果**。`vertical-align: top`**`line-height`
*
最后,`vertical-align`还接受相对于基线升高或降低框的数值。最后一个选项可能会派上用场。
CSS无所不能
我们已经讨论了如何协同工作,但现在的问题是:字体规格是否可以通过 CSS 控制line-height
?vertical-align
简短的回答:没有。即使我真的希望如此。无论如何,我认为我们必须玩一下。字体指标是恒定的,所以我们应该能够做一些事情。
例如,如果我们想要一个使用 Catamaran 字体的文本,其中大写高度正好是 100px 高怎么办?似乎可行:让我们做一些数学运算。
首先我们将所有字体指标设置为 CSS 自定义属性4,然后计算font-size
得到 100px 的大写高度。
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);
}
*
很简单,不是吗?但是,如果我们希望文本在视觉上位于中间,以便剩余空间均匀分布在“B”字母的顶部和底部怎么办?为此,我们必须`vertical-align`根据上升/下降比率进行计算。
首先,计算content -area的高度:line-height: normal
**
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);
}
最后,我们设置 desiredline-height
并在保持垂直对齐的同时计算它:
p {
…
/* desired line-height */
--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;
}
*
[在 JSBin 中查看结果](http://jsbin.com/tufatir/edit?css,output)
请注意,此测试仅用于演示目的。你不能依赖这个。很多原因:
- 除非字体规格是常量,否则浏览器中的计算不是¯\ (ツ) /¯
- 如果未加载字体,后备字体可能具有不同的字体规格,并且处理多个值将很快变得非常难以管理
总结
我们学到了什么:
内联格式化上下文真的很难理解
所有内联元素都有 2 个高度:
- 内容区域(基于字体规格)
- 虚拟区域(
line-height
) - 毫无疑问,这两个高度都无法想象。(如果你是一名 devtools 开发者并且想从事这方面的工作,那可能会很棒)
line-height: normal
基于字体规格line-height: n
可以创建一个小于内容区域的 虚拟区域**vertical-align
不是很可靠line-box的高度是根据其子项
line-height
和vertical-align
属性计算的我们无法使用 CSS 轻松获取/设置字体指标
但我仍然喜欢 CSS :)