一次内联元素错位引发对line-height的思考

avatar
@滴滴出行

作者:李一睿

line-height 对于一个前端小可爱来说,应该是一个会经常碰面的老朋友了。可是有一天,我突然发现自己好像对他没那么了解,他也没有外表看起来的那么简单。

事情的经过是这样的……

在偶然一次工作中,我写了这样的模板:

<div>
    <span class="name">重大疾病险</span>
    <span class="tip">保额每月可累计</span>
</div>
div{
    font-family: "PingFang SC";
}
.name{
    line-height: 20px;
    font-size: 20px;
}
.tip{
    display: inline-block;
    line-height: 20px;
    font-size: 14px;
}

两个相邻的内联元素,字体一大一小,行高相同,由于第二段文字需要有字多情况就自动去下一行的效果,所以第二个spaninline-block我畅想的结果是,两个 span 高度都是 20px, div 高度也是 20px,多么完美。但结果往往不近人意……

div 的高度怎么是 28??

再一看子元素,一个 28 一个 20??

  • 疑问1:难道是 line-height 对行内元素不生效??

    然而规范告诉我,对于非替代的 inline 元素,它用于计算行盒(line box)的高度。

  • 疑问2:既然生效了,为什么审查元素看着是 28,不是 20 呢?

    对于文本来说,存在一个内容区域(content area)。你可以理解为,用光标选中这行文字时带背景的区域。他同时受 font-family 和 font-size 的影响。就算是相同的字号,如果字体不同,‘你所看到’的高度也是不一样的,同学们可以自己尝试一下。这里强调你所看到的,也就是我们这里的 28px,它其实是这个文本内容区的高度,而内容区的高度并不是真正的高度,也就是说,它不会影响这个元素真正的尺寸,也不会撑起父元素的高度,所以只是一个你看起来的高度。而对于行内元素来说,真正影响它高度的就是line-height

    在这里,我们可以通过改变他们的 font-family 来证实一下。

      div{
          font-family: "HelveticaNeue";
      }
      .name{
          line-height: 20px;
          font-size: 20px;
      }
      .tip{
          display: inline-block;
          line-height: 20px;
          font-size: 14px;
      }
    

    可以看到,更改了字体之后,.name的高度由28变成了24,.tip由于是inline-block所以高度没变,但是父div的高度依然是28px,说明content area并不会影响父元素的真实高度。

  • 疑问3:那如何看到它真正的高度?

    你可能也发现了,第二个 span 是inline-block,它的高度就是 20px,那我们将第一个span也设置成inline-block来看一下。

    果然,它的高度终于变成了我们期望的 20px。
    那我们再来看一下父元素div的高度:

    !!怎么还是 28px??

  • 疑问4:两个子元素都已经是 20px 了,为什么父元素还是 28px?

    这里先声明两个问题:

    1. 证明前面疑问2中说的没错,content area 是不会影响实际尺寸的,现在我们元素都是 20px 了,可父元素依然还是 28px。
    2. 我们将第一个 span 设置成inline-block之后,可以看到它的尺寸已经变成了 20px,但这并不代表这它就没有 content area 了,或者它的 content area 变成了 20px。你可以理解为我们给他外面包了一层盒子,你可以看到盒子的真正的尺寸,而 content area 在它里面,你只是看不到罢了。更何况,它是一个影响不了谁的东西。

    接着回到我们的问题,仔细看上图,可以发现,两个高度都是 20px 的子元素,上下都有一定的间隙;再仔细看,发现它俩没对齐,红色背景的靠上,粉色背景的靠下,它俩之间稍微有些错位。就是这些,让我们的父元素被撑成了 28px。那它们又是从哪里来的呢?

    既然是对不齐导致父元素被撑高了,那 CSS 中相关对齐的属性,我们很容易想到是vertical-align,难道是它搞得鬼?那我们试着改一下它们的vertical-align

    vertical-align:top,上面对不齐了

    vertical-align:middle,好像跟之前差别不大……

    虽说这些属性值都没能让他们对齐,但可以发现确实是与它有关的,可能是我们哪里使用不当,影响了这个属性?那我们就去了解一下vertical-align是怎么对齐的,起初我们不设置的时候,会取它的默认值:baseline—— 基线对齐。

    内联元素默认是基线对齐的,而基线就是指行框盒子中字母'x' 的下边缘:

    诶??再仔细看看我们之前对不齐的那张图,你会发现,虽然元素没对齐,但是文字下端是对齐的,这不就是正常的基线对齐吗!这下就说的通了。

    划重点!!

    行内元素的对齐方式默认是基线对齐,也就是都与 x 的下边缘对齐。这两个 span 的高度是一样的,都是 20px,然而重点就在于它们的字体大小不一样。而文本在如果设置了line-height,是会基于line-height居中的,也就是说,它们分别在各自高度相同框框中居中,但由于字号大小不一样,所以文字底部是对不齐的,但他们又需要基线对齐,所以它俩开就必须要错位一下。

    这里引用一下鑫旭大神的图:

  • 疑问5:那这是不是就说明,我们把两个元素的行高设置成不一样,就可以不错位把父元素撑高了?

    这里我们分别给两个span设置与自己字体大小一样的行高:20px 和 14px,可以看到它俩已经没有向外错位了,但是父元素的高度并没有如期,还是 28px。怎么回事?难道还有别的东西再影响它?

    你猜对了,但这是个我们看不见的东西。引用一下张鑫旭大神的叫法——幽灵空白节点。

    “幽灵空白节点”是内联盒模型中非常重要的一个概念,具体指的是:在 HTML5 文档声明 中,内联元素的所有解析和渲染表现就如同每个行框盒子的前面有一个“空白节点”一样。这 个“空白节点”永远透明,不占据任何宽度,看不见也无法通过脚本获取,就好像幽灵一样, 但又确确实实地存在,表现如同文本节点一样,因此,我称之为“幽灵空白节点”。

    既然看不见,那我们可以拿一个看的见的 x 字符来假装它。

      <div>
          <em>x</em>
          <span class="name">重大疾病险</span>
          <span class="tip">保额每月可累计</span>
      </div>
    
      div{
          font-family: "PingFang SC";
      }
      em{
          display:inline-block; /*为了审查元素看到的不是content area*/
          font-style:normal;
      }
      .name{
          line-height: 20px;
          font-size: 20px;
      }
      .tip{
          display: inline-block;
          line-height: 20px;
          font-size: 14px;
      }
    

    这个 'x' 字号和行高都没有设置,字号是继承下来的 20px,元素行高默认是 1.x(1.x 倍的 font-size),具体数值还有待研究,但绝对是 >1 的,由图可见,什么都没有设置的情况下,这个 'x' 的高度就是 28px,也就代表着,那个幽灵空白节点的高度就是 28px。

    那我们将幽灵空白节点的行高设小一些不就好了?可是这个幽灵节点,看不见摸不着,怎么设啊。别忘了,line-heigt是可继承性的,我们给父div设置line-heigt,幽灵节点自然就会继承。

如何解决

明白了上述的疑问以及原理,接下来就看下怎么解决这个问题。

解决一

我们可以给父divline-height设置一个很小的值

div{
    line-height: 5px;
    font-family: "PingFang SC";
}
em{
    display:inline-block; /*为了审查元素看到的不是content area*/
    font-style:normal;
}
.name{
    line-height: 20px;
    font-size: 20px;
}
.tip{
    display: inline-block;
    line-height: 20px;
    font-size: 14px;
}

这样,幽灵节点的行高就只有 5px,父元素行高 20px,没毛病。但这个方法还不够完美,因为你需要想一下这个很小的值……

解决二

当 line-height 设置为一个无单位的数值时,表示是某倍的font-size。

div{
    line-height: 1;
    font-family: "PingFang SC";
}
em{
    display:inline-block; /*为了审查元素看到的不是content area*/
    font-style:normal;
}
.name{
    line-height: 20px;
    font-size: 20px;
}
.tip{
    display: inline-block;
    line-height: 20px;
    font-size: 14px;
}

给父元素设置line-height为1,这样,子元素高度都是自己的font-size,包括幽灵空白节点。这样就不会再有人无意间撑高父元素了。当然,除非你的幽灵空白节点继承下来了一个很大的 font-size, 这点你得明白。

解决三

既然行高可以与 font-size 有关,font-size 也是可继承的,那我们将父元素的 font-size 设为 0

div{
    font-size: 0px;
    font-family: "PingFang SC";
}
em{
    display:inline-block; /*为了审查元素看到的不是content area*/
    font-style:normal;
}
.name{
    line-height: 20px;
    font-size: 20px;
}
.tip{
    display: inline-block;
    line-height: 20px;
    font-size: 14px;
}

可以看到,我们模仿的那个幽灵节点已经没有了,宽高都是 0 了,父元素的高度也自然就回到了 20px。

总结

以上,可以说是我对行内元素对齐问题的一些思考总结吧。如果你还有疑问,可以再多看几遍,自己尝试一下再继续思考思考。这个问题我也是来来回回想了好多遍,也欢迎大家评论提问,我们可以一起讨论~

前端小白,有讲解错误或不全之处,还望大佬们嘴下留情,欢迎指正~