关于vertical-align属性

前言

关于vertical-align属性被用得最多的场景就是用于对一些内联元素的垂直居中显示,然而事情永远不是我们想的那样简单,给了一个middle就能永远如我们所愿,很多时候基本就会发现图片和文字压根就没有垂直居中对齐……这个时候基本上都不知道为啥,试了一些其他属性值之后如果未果那么基本上就会用上flex布局暴力居中了,我一般就是这样做的  ̄□ ̄||。

vertical-align的作用

对于内联元素

内联元素包括display: inlinedisplay: inline-*和匿名内联元素(如文本节点等);

vertical-align可以设置内联元素与其容器元素(父元素或line boxes)在垂直方向的对齐方式;要理解对齐方式,首先要理解以下几个概念:

  • 基线
  • x高度
  • inline boxes
  • line box

相关概念

基线和 x 高度

img

可以看一下维基百科的一张图[1],一目了然;x高度指的就是小写字母x的高度,而基线指的就是小写字母x底部边缘处;这张图的其他概念也扩展一下:

  • ascent:升部;即高于小写x字母以上的部分;
  • ascent height:升部高度;某个字符位于升部区域的高度;
  • descent:降部;即低于小写x字母以下的部分;
  • descent height:降部高度;某个字符位于降部区域的高度;

inline boxes 和 line boxes

img

这里借用张老师的一张图[2],也是一目了然;

  • inline box:其实就是各种内联元素外面包裹的一层盒子,其高度与font-sizeline-height有关;

  • line box:顾名思义,指的就是一行内联元素所包裹的盒子,可以包含多个inline box。可以用鼠标选中内联元素,高亮的区域的高度就是其对应的line box的高度。

vertical-align的属性值

vertical-align的对齐对象有两类,因此属性值作用的对象也有两类,一类是父元素,一类是line boxes(即每一行);

对齐对象为父元素

The following values only have meaning with respect to a parent inline element, or to the strut of a parent block container element.[3]

  • baseline默认值,使内联元素盒子(inline box,下同)基线与父元素的基线对齐;不过父元素的基线位置有多种情况,需要单独说明;
  • middle:使内联元素盒子的中线与父元素基线向上0.5x高度的位置对齐;很明显,我们平时把middle理解为内联元素的中线与父元素的中线对齐是错误的
  • text-top:使内联元素盒子的顶部与父元素content area的顶部对齐;
  • text-bottom:使内联元素盒子的底部与父元素content area的底部对齐;
  • sub:使内联元素盒子的基线与父元素的下标基线对齐;
  • super:使内联元素盒子的基线与父元素的上标基线对齐;
  • <length>:使内联元素盒子的基线与父元素基线向上给定长度的位置对齐;可以为负值,负值则代表基线向下给定长度的位置;
  • <percentage>:与<length>同理,百分比相对的是内联元素的line-height

关于 inline box

这里内联盒子的范围可以参考W3C的官方定义:

In the following definitions, for inline non-replaced elements, the box used for alignment is the box whose height is the ‘line-height’ (containing the box’s glyphs and the half-leading on each side, see above). For all other elements, the box used for alignment is the margin box.[3:1]

简言之,对不可替换元素,这个inline box的高度就是line-height;反之则是margin-box

On a block container element whose content is composed of inline-level elements, ‘line-height’ specifies the minimal height of line boxes within the element. The minimum height consists of a minimum height above the baseline and a minimum depth below it, exactly as if each line box starts with a zero-width inline box with the element’s font and line height properties. We call that imaginary box a “strut.”[4]

关于没有基线的元素

对于没有基线的元素来说,其基线用margin-box区域的底边缘来替代!然而对于表现形式为inline-block的内联元素来说,其基线的确定要稍微特殊一点:当inline-block元素内部没有任何内联子元素或者overflow的值不为visible时,其基线位置还是margin-box的底边缘,否则就是其内部最后一个line box的基线位置!

The baseline of an ‘inline-block’ is the baseline of its last line box in the normal flow, unless it has either no in-flow line boxes or if its ‘overflow’ property has a computed value other than ‘visible’, in which case the baseline is the bottom margin edge.[3:2]

关于 content area 及其顶部位置

content area是什么?content area的顶部位置又是如何确定的?本来我也没想过深究这些,直到我准备查看一下text-top属性值的效果时,我简直一脸懵逼,不知道发生了什么……看个例子:

1
2
3
4
5
6
7
8
9
.demo {
width: 200px;
height: 50px;
line-height: 50px;
background-color: lightcyan;
}
.pic {
vertical-align: text-top;
}
1
2
3
<div class="demo">
<img class="pic" src="https://placekitten.com/40/40">
</div>

img

很明显<img>的对齐区域应该是其margin-box,那么为何其父元素的content area的顶部不是<div>的顶部?可能的解释就是,其对齐的父元素不是外层的<div>,而是line box!那么如何确定line boxcontent area

关于content area的高度,W3C有这么一段说明:

The height of the content area should be based on the font, but this specification does not specify how. A UA may, e.g., use the em-box or the maximum ascender and descender of the font.[3:3]

据我观察,谷歌浏览和火狐浏览器应该都是采用了字体的最大升部高度和最大降部高度组成高度;即:

content area height = x高度 + 最大升部高度 + 最大降部高度

因为这个line box的顶部位置正好可以通过line-height - font-size * 1.4计算得到(这个1.4根据具体字体可能有差异);

content area的高度小于line box的高度时,那么line boxcontent area顶部就是:

line box height - content area height

反之,那么line boxcontent area顶部就是line-box的顶部;

可以利用上面的公式大概计算一下,怎样可以使得上述的text-top能够对齐到<div>的顶部,即line boxcontent area的高度等于line-height;由于line-height设置为50px,所以推出font-size50/1.4,约等于36px;看下font-size36px的情况:

img

差不多就是这个逻辑;

对齐对象为 line box

  • top:内联元素及其后代的顶部与line box的顶部对齐;
  • bottom:内联元素及其后代的底部与line box的底部对齐;

除了上面的简单定义,W3C的规范里还提到了一点:

The inline-level boxes are aligned vertically according to their ‘vertical-align’ property. In case they are aligned ‘top’ or ‘bottom’, they must be aligned so as to minimize the line box height. [5]

也就是说,使用topbottom对齐时,会使得line box的高度保持在尽量小的程度;而line box最小高度就是line-height,这一点规范上也有说明:

On a block container element whose content is composed of inline-level elements, ‘line-height’ specifies the minimal height of line boxes within the element[4:1]

关于 line box 及其高度

关于line boxW3C有这么一段定义:

In an inline formatting context, boxes are laid out horizontally, one after the other, beginning at the top of a containing block. Horizontal margins, borders, and padding are respected between these boxes. The boxes may be aligned vertically in different ways: their bottoms or tops may be aligned, or the baselines of text within them may be aligned. The rectangular area that contains the boxes that form a line is called a line box.[6]

简言之,就是每一行上包裹这一行内所有inline box的那个区域就是line box;而line box的高度则是由其内部最靠上inline box顶部到最靠下inline box底部之间的距离(因为可能各个inline box之间的高度和垂直对齐方式不一致):

The height of a line box is determined as follows:

  1. The line box height is the distance between the uppermost box top and the lowermost box bottom. (This includes the strut, as explained under ‘line-height’ below.)[5:1]

对于单元格元素

单元格元素包括<td>display: table-cell的元素;

vertical-align除了可以设置内联元素的垂直对齐方式外,还能用于设置单元格的垂直对齐方式,其属性值有:

  • baseline:使单元格的基线与当前行中其它所有以基线对齐方式的单元格的基线对齐;
  • top:使单元格的padding-box的顶边缘与当前行的顶部对齐;
  • bottom:使单元格的padding-box的底边缘与当前行的底部对齐;
  • middle:使单元格的padding-box居中;

(幽灵)空白节点

空白节点的特征就是:本身是一个inline box,宽度为0,但是具有(继承fontline-height属性;因此空白节点本身是看不见的。关于这种空白节点,貌似找不到很明显的官方定义;张老师把这种空白节点称之为“幽灵空白节点”,因为这些空白节点都是无意或者html在解析的过程中自动产生的,且不能直接控制,所以谓之“幽灵”。

理解空白节点的存在对于弄清楚之前使用vertical-align: middle等属性值为何不生效(或者不符合预期)是必须的。

空白节点的来源

大致有两处来源:

  • 标签后面换行符空格产生的;如:

    1
    2
    3
    4
    5
    6
    7
    8
    .demo {
    width: 200px;
    background-color: lightcyan;
    }
    .pic {
    width: 40px;
    height: 40px;
    }
    1
    2
    3
    <div class="demo">
    <img class="pic" src="https://placekitten.com/40/40">
    </div>

    img

    可以看到图片底部留有空隙,空隙产生的原因就是<img>后面进行了换行,因此html解析后多出一个空白节点;空白节点虽然宽度为0,但是继承了line-height属性,因此其高度就是line-height,所以<img>元素的margin-box底部会与空白节点的inline box基线进行对齐!

  • line box内部默认第一个inline box就是空白节点;

    exactly as if each line box starts with a zero-width inline box with the element’s font and line height properties. We call that imaginary box a "strut"[4:2]

    W3C把这种空白节点称之为“strut”,意为“支柱”;可能是撑起行高的作用。

空白节点的影响

导致图片不能正常居中

有时候我们可能想使用line-height等于height加上vertical-align: middle使图片在某区域中垂直居中这种套路,结果发现图片总是中间偏下一点:

1
2
3
4
5
6
7
8
9
10
.demo {
width: 200px;
height: 50px;
background-color: lightcyan;
}
.pic {
width: 30px;
height: 30px;
vertical-align: middle;
}
1
2
3
<div class="demo">
<img class="pic" src="https://placekitten.com/40/40">
</div>

img

这个就是空白节点捣的鬼,由于空白节点继承了line-height,进而有了基线位置,因此line box的基线位置就变了空白节点的基线位置,然而字体基线位置并不是在中线位置往下0.5x高度的地方,而是更下一点的位置,也就是“基线下沉”。因此,此时基线往上0.5x高度之后的位置并不是inline box的中线位置,而且偏下一些的位置;至于到底偏下多少,这取决于字体及字体大小

img

可以在<img>加上一个x来查看基线位置(可以当做“显形”的空白节点),蓝色框就是文本节点的inline box区域,可以看到基线位置明显偏下(inline box整体居中),因此<img>对齐的地方根本不是inline box的中线位置,所以就会偏下。

解决办法:虽然不能直接控制空白节点的样式,但是由于空白节点继承fontline-height属性,所以可以操纵外层元素的font-size来影响空白节点的基线位置。设置外层font-size: 0,可以使得空白节点的line-height0,因此其高度也为0,那么所有的“线”都重合了,处于line box的中线处,所以<img>此时对齐的位置就是line box的中线:

img

其它问题

图标和文字不能垂直居中对齐

同一行中,混有图标和文字时,若使父级元素的height等于line-height,图标高度也等同于父级元素的高度,但是可以看到文字并非和图片一样垂直居中,而是偏下一些。如:

1
2
3
4
5
6
7
8
9
10
11
.demo {
width: 200px;
line-height: 20px;
height: 20px;
background-color: lightcyan;
}
.pic {
width: 20px;
height: 20px;
vertical-align: middle;
}
1
2
3
4
<div class="demo">
<img class="pic" src="./test.png">
<span>x开头的一段话</span>
</div>

img

可以分析一下,由于vertical-align的默认值就是baseline,因此图片和文字都是基线对齐其父元素,即外层line box的基线位置;从上图可以很明显看出,此时line box的基线位置就是<div>区域的底部边缘,因此文字基线就会对齐底部,而非想象中的居中。

那么把对齐方式都改为middle又如何?

img

显然,由于基线下沉的影响,文字对齐的位置会相对中间线偏下一点点。

解决办法:只需要将图标的对齐方式设置为top即可,此时图标和文字就会居中对齐:

img

因为top对齐会使得line box的高度保持在最低,此时由于inline box的高度都没有超过line-height,因此此时line box的高度就是line-height的高度。

为何修改对齐方式会导致line box基线位置变化

虽然可以说x底部边缘位置就是line box的基线所在位置,但是为何修改inline box的对齐方式会导致line box的基线位置发生改变?关键就在于如何确定line box的基线位置,然而要命的就是CSS规范里面竟然没有定义line box的基线位置!!!

there are multiple solutions and CSS 2.1 does not define the position of the line box’s baseline[5:2]

看到这里我突然觉得为啥围绕着竖直对齐为何会产生那么多说不清的地方了……因为很多对齐方式基本上都是根据line box的基线位置进行对齐,然而line box的基线位置居然没有官方定义。

???

然而实际上浏览器在实现渲染的时候,肯定会有关于line box基线位置的定义的,不过不知道该去哪里能找到这份定义,不然就一直在这个地方纠结不清了。

后话

为何使用vertical-align属性的时候会心里没底,查完那么多资料我终于明白了,因为牵扯到的规范实在是太多了!

还有,这次之所以引用了那么多的W3C规范,是因为根据MDN的资料和网上一些文章压根不足以解答心中的疑惑。因此,去查看一手资料是最直接了当的方式,也免得道听途说误解了什么。不过这样查资料确实挺耗费时间的,但得到的收获也少不了。

参考文档


  1. X字高 - 维基百科,自由的百科全书 ↩︎

  2. CSS float浮动的深入研究、详解及拓展(一) « 张鑫旭-鑫空间-鑫生活 ↩︎

  3. https://www.w3.org/TR/CSS2/visudet.html#propdef-vertical-align ↩︎ ↩︎ ↩︎ ↩︎

  4. https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height ↩︎ ↩︎ ↩︎

  5. https://www.w3.org/TR/CSS2/visudet.html#line-height ↩︎ ↩︎ ↩︎

  6. https://www.w3.org/TR/CSS2/visuren.html#inline-formatting ↩︎