深入了解CSS字体度量,行高和vertical-align

发布于 大漠

line-heightvertical-align在CSS中是两个简单的属性。如此简单,大多数人都相信自己已经完全理解它们是如何工作的以及如何使用它们。但事实上并不如此。他们其实很复杂,也是CSS中难点之一,而且也是CSS中特性之一:内联格式化上下文(inline formatting context)

比如可以设置line-height带有长度单位的值或一个无单位的值,但其默认值是normal。那么在CSS中normal是什么呢?我们常常认为它是(或者应该是)1或者1.2,甚至也可以说,CSS规范都不清楚是哪一个。我们也知道,没有单位的line-height是相对于font-size的,但问题是,font-size: 100px;在使用不同的字体(font-family)表现的行为是不一样的,所以line-height总是相同或不同的吗?真的是1还是1.2吗?另外vertical-alignline-height的影响又是什么呢?

要深入研究CSS的机制可以说没有这么简单......

首先来聊font-size

首先来看一个简单的HTML代码,一个<p>标签中包含了三个<span>标签,每个<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,但使用不同的font-family,但渲染出来的line-height是不同的:

即使我们意识到这种行为,但还是不清楚为什么font-size:100px时元素的height不是100px?我测量发现:Helvetica字体的高度是115pxGruppo字体的高度是97pxCatamaran字体的高度是164px

起初似乎有点奇怪,但它是完全可预期的。这主要还是font-family的原因。那就要搞清楚它是如何工作的:

  • 字体定义其em-square,每个字符将会绘制出自己的容器。这个正方形使用相对单位和生成一个1000单位。但它也可以是10242048或者其他
  • 根据其荐对单位,字体的度量可以根据一些设置(ascender,descender,capital height,x-height等)来决定。注意,有些值是em-square之外的值
  • 在浏览器中,相对单位是用于缩放用来适应所需的font-size

让我们来看Catamaran字体,并且在FontForge中来看这个字体的度量参数:

  • em-square是1000
  • 上升(ascender)是1100和下降(descender)是540。相同的测试下,浏览器使用HHead Ascent/Descent值(Mac)和Win Ascent/Descent值(Windows),这些值可能不同。我们还需要注意,Capital高度是640和x-height的值是485

这意味着Catamaran字体在1000个单位的em-square使用了1100 + 540个单位,也就是说font-size:100px的时候,其高度是164px。这个计算高度定义了元素内容高度(在这篇文章中其它部分引用这个术语content-area)。你可能想到是内容区域相当于background属性。

我们也可以预测,大写字母是68px高度(680个单位)和小写字母(x-hegiht)是49px高度(485个单位)。因此,1ex = 49px1em = 100px,而不是164px(值得庆幸的是,em是基于font-size计算,而不是height)。

在继续深入之前,先要了解这涉及到什么?当<p>元素呈现在屏幕上,它根据它的宽度可以有很多线。每一行是由一个或多个行内元素(HTML标签元素或匿名内联元素文本内容)组成,专业术语称为行盒(line-box)。line-box的高度是基于它的子元素高度的。浏览器为每个行内元素计算的高度都是line-box(子元素的最高点到最低点)。因此line-box的总高度足以包含所有子元素(默认情况下)。

每个HTML元素实际上是一个line-box的堆栈。如果你知道每个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>

它会生成三个line-box:

  • 第一个和最后一个每个包含一个匿名内联元素(文本内容)
  • 第二个包含了两个匿名内联元素和三个<span>

<p>元素(黑色边框)产生了一个line-box(白色边框),其包含了内联元素(实心边框)和匿名内联元素(虚线边框)。

我们清楚的看到,第二个line-box明显比其他的line-box要更高,根据子元素的内容区域(content-area)计算得来,更具体地说,是使用了Catamaran字体。

困难的是line-box创建部分是我们无法看到的,也不是用CSS控制它。即使在::first-line应用了background也无法直接在视觉上看到第个line-box的高度。

line-height问题

直到现在,我们介绍了两个概念:content-arealine-box。如果仔细阅读了前面的内容,你应该知道line-box的高度是根据子元素高度来计算,而且我并没有说是子元素的内容区域(content-area)的高度。这是有很大区别的。

尽管这听起来可能有些奇怪,内联元素有两个不同高度:内容区域(content-area)高度和虚拟区域(virtual-area)高度(这是我发明的术语virtual-area高度,你在规范中是找不到任何相关的内容)。

  • 内容区域高度是由字体来决定的(前面介绍过)
  • 虚拟区域(virtual-area)高度是line-height,它的高度用于计算line-box的高度

行内元素有两个不同的高度。

也就是说,line-height普遍的看法是不同基线(baseline)的距离。在CSS中,它并不是这样。

计算虚拟区域(virtual-area)和内容区域(content-area)高度差称为leading。leading添加在内容区域顶部,另一半添加在内容区域底部。因此,内容区域总是在虚拟区域的中间。

根据其计算值,line-height(virtual-area)相同情况下比content-area更高或更低。对于较小的virtual-area,leading是负值和line-box要比它的子元素更小。

还有其他的内联元素:

  • 替代内联行内元素(<img><input><svg>等)
  • inline-block元素
  • 行内元素参与特定格式化上下文(如,Flexbox元素,和所有的Flex项目)

对于这些特定的行内元素,高度计算基于他们的heightmarginborder属性。如果hegiht的值是auto,然后使用line-height时content-area严格上等于line-height

无论如何,我们仍然面临的问题是line-heightnormal值是多小?答案是,其计算content-area高度还是依据于里面的字体来度量。

我们回到FontForge。Catamaran的em-square是1000,但我们看到ascender/descender的值:

  • 生成的Ascent/Descent: ascender是770,descender是230。用于绘制字符(OS/2)
  • 度量的Ascent/Descent: ascender是1100,descender是540。用于内容区域高度(hhea和OS/2)
  • 度量线的间距:通过Ascent/Descent度量使用line-height: normal(hhea)

在我们的示例中,Catamaran字体定义了0个单位的线间距(Line Gap),因此line-height: normal的值将等于内容区域,也就是1640个单位或1.64

作为比较,Arial字体的一个em-square是2048个单位,其ascender是1854,descender是434,线间距是67。这意味着,font-size: 100px的内容区域是112px1117个单位)和line-height115px1150个单位或1.15)。所有这些度量都是特殊字型,由字体设计师来设置。

显而易见,设置line-height:1是一个非常糟糕的做法。我提醒你,font-size没有单位的观念是相对的,但内容区域不是相对的以及处理虚拟区域小于内容区域有很多问题存在。

但并是只有line-height:1。不论真假,我电脑上安装了1117种字体(是的,我安装了所有的Google Web字体),其中1059种字体,占全部字体的95%左右,计算的line-height大于1。它们计算line-height是从0.6183.378。你得记住,是3.378

line-box计算的小细节:

  • 对于内联元素,paddingborder增加了其background区域,但不会增加内容区域高度(甚至是line-box高度)。因此,你在屏幕上看到的不一定就是内容区域。margin-topmargin-bottom对内联元素不生效。
  • 对于行内替代元素,inline-block和blocksified行内元素,paddingmarginborder都会增加高度,所以内容区域和line-box的高度也会增加

vertical-align:一个属性控制一切

前面我没有提到vertical-align属性,即使它是计算line-box高度的一个重要因素。我们甚至可以说,vertical-align属性对于行内格式化上下文中的leading有很大的作用。

vertical-align的默认值是baseline。你注意到度量字体的ascender和descender?这些值是基于baseline,具有一定的比例。那么ascender和descender之间的比例真的是50/50,它可能会产生意想不到的结果,例如所有兄弟元素。

先从这个代码开始:

<p>
    <span>Ba</span>
    <span>Ba</span>
</p>

p {
    font-family: Catamaran;
    font-size: 100px;
    line-height: 200px;
}

两个<span>元素继承了<p>元素的font-familyfont-size和固定的line-height。基线将会匹配以入line-box的高度等于他们的line-height

如果第二个元素设置更小的font-size呢?

span:last-child {
    font-size: 50px;
}

这听起来很奇怪,但默认基线对齐可能导致更高的line-box,如下图所示。我提醒你,line-box的高度是从它的子元素最高点和最低点计算。

有一个观点可以得到支持,那就是line-height设置不带任何单位的值,但有时你需要做一个完美的Vertical-rhythm。说实话,不管你选择什么,你总是会有困难的。

看看另一个例子。<p>元素的line-height值设置了200px,并且包含了一个<span>元素,这个<span>元素继承了<p>元素的line-height

<p>
    <span>Ba</span>
</p>

p {
    line-height: 200px;
}
span {
    font-family: Catamaran;
    font-size: 100px;
}

line-box有多高?我们期望的是200px,但如果不是,我们得到的又是什么?这里不同的是<p>元素有自己的字体(默认是serif)。<p><span>之间的基线可能是不同的,因此line-box的高度是高于预期的。这是因为浏览器给每个line-box计算都是开始于一个任意字符。规范中称之为strut

一个看不见的角色,但的确是会有可见的影响。

就我自己一些经历,我们将面临同样的问题,那就是兄弟元素。

基线对齐是完了,但vertical-align:middle可以拯救它们?可以阅读规范:

Middle “aligns the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent”.

基线的比例不同,以及x-height比例,所以中间对齐是不可靠。最坏的情况下,在大多数的情况下,中间就从来没有真的中间过。这里面有太多的因素参与其中,使用CSS是不能设置这些因素的(x-height,ascender和descender比例等)。

它有四个值,这可能在某些情况下是有用的:

  • vertical-align: top | bottom和line-box的顶部或底部对齐
  • vertical-align: text-top | text-bottom和内容区域的顶部或底部对齐

注意了,在所有情况下它都是在虚拟区域中,所以是看不见的高度。看看这个简单的示例,使用vertical-align:top,看不见的line-height可能产生一些很奇怪的结果。

最后,vertical-align还能接受数值,提高或降低盒子的基线。最后一个选项可以派上用场。

CSS 无所不能

我们已经讨论过了line-heightvertical-align在一起是如何工作,但现在的问题是如何使用CSS来控制字度的度量指标?简短的回答:没有。即使真的如此,我也想我们应该可以做些什么?那么有关于字体度量,我们应该能够做些什么?

例如,如果我们想要给文本使用Catamaran字体,可以把其capital高度扩展到100px?通过一些数学计算,似乎可行。

首先设置度量字体的五个自定义属性,然后计算font-size,从而得到capital高度是100

p {
    /* font metrics */
    --font: Catamaran;
    --capitalHeight: 0.68;
    --descender: 0.54;
    --ascender: 1.1;
    --linegap: 0;

    /* desired font-size for capital height */
    --fontSize: 100;

    /* apply font-family */
    font-family: var(--font);

    /* compute font-size to get capital height equal desired font-size */
    --computedFontSize: (var(--fontSize) / var(--capitalHeight));
    font-size: calc(var(--computedFontSize) * 1px);
}

很简单,不是吗?但如果我们想要让文本在可视区居中,让剩余的空间均分在"B"字的顶部和底部,应该怎么做呢?为了达到这一目的,我们必须基于ascender和descender比例计算出vertical-align

首先,计算line-height:normal和内容区域的高度。

p {
    …
    --lineheightNormal: (var(--ascender) + var(--descender) + var(--linegap));
    --contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}

这时,我们需要:

  • 大写字每底部距离底部边缘的距离
  • 大写字母顶部距离顶部边缘的距离

像这样:

p {
    …
    --distanceBottom: (var(--descender));
    --distanceTop: (var(--ascender) - var(--capitalHeight));
}

我们现在可以通过距离乘以font-size计算出vertical-align

p {
    …
    --valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
    vertical-align: calc(var(--valign) * -1px);
}

最后,我们设定所需的line-height和计算它,保持一个垂直对齐:

p {
    …
    /* desired line-height */
    --lineheight: 3;
    line-height: calc(((var(--lineheight) * var(--fontSize)) - var(--valign)) * 1px);
}

添加一个图标和字母"B"垂直对齐,现在很容易就能做到:

span::before {
    content: '';
    display: inline-block;
    width: calc(1px * var(--fontSize));
    height: calc(1px * var(--fontSize));
    margin-right: 10px;
    background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');
    background-size: cover;
}

示例的地址可以点击这里

**注意:**这个测试只是出于演示目的。你不能依赖于此。如果字体不加载,备用字全有可能具有不同的字体度量参数,它就没法正常工作了。

在一部分示例中,大家看到很有以--开头的,这是CSS的原始变量,也称之为CSS自定义属性。如果在此之前你从未接触过这方面的内容,建议你先点击这里进行了解。

总结

这篇文章我们学到了什么:

  • 行内格式化上下文真的很难理解
  • 所有行内元素都有两个高度
  • 内容区域(content-area)基于字体的度量参数
  • 虚拟区域(virtual-area)就是line-height
  • 这两个高度是无法可视的(如果你通过开发者工具,你可以看到)
  • line-height:normal是基于字体度量参数
  • line-height: n有可能创建一个虚拟区域比内容区域更小
  • vertical-align不是很可靠
  • 一个line-box的高度计算是基于它的子元素的line-heightvertical-align属性
  • 我们没有办法直接通过CSS来获取或设置字体的度量参数
  • 未来可能会有一个垂直对齐的规范来解决这些看似问题的问题:Line Grid Module

相关资源

本文根据@iamvdo的《Deep dive CSS: font metrics, line-height and vertical-align》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/css/css-font-metrics-line-height-and-vertical-align.htmlnike free run 5.0 youth