数十种利用 CSS 不同特性垂直居中的方法
!注意:本文中各专业名词待校正,现可能出现不一致的情况,请注意。
前置知识
在我们讨论如何进行垂直居中前,先整理一下将使用到的术语,以避免可能的理解差异。
盒子树和盒子
盒子树是用于 CSS 渲染的一个中间结构,表示了被渲染文档的格式化结构。每个盒子树中的盒子表示了其空间和/或时间上对应的元素,而盒子树中的每个文本序列同样表示了其文本节点对应的内容。
包含块
包含块是一个矩形,规定了其相关盒子的尺寸和位置。特别指出,包含块不是盒子(它是个矩形),但它通常来源于盒子的尺寸。每个盒子都根据其包含块被给定了位置,但不会被此包含块限制。它可以溢出。短语“一个盒子的包含块”指的是盒子所在处的包含块,并不是说其生成的包含块。
显示类型
我们称元素样式的 display
属性设定的元素表现类型为显示类型。
垂直居中分类
我们将数十种方法总结为以下七类,每类可能附带不同细分的方法:
1. 指定绝对位置
第一种方法显得比较笨,它是这样的:
在父元素和子元素的宽高对开发者已知时,可以使用绝对定位加些许偏移调整的方式使其子元素居中(margin,或是自己计算偏移的量修改绝对定位)。虽然现代前端开发不常用了,但是此方法的兼容性是最好的,而且不会占用子元素的 transform
属性。margin
兼容性和 position
的兼容性可点击查看。
.parent {
position: relative;
width: 100vw;
height: 100vh;
}
.child {
position: absolute;
top: 50%;
left: 50%;
margin-left: -20vw;
margin-top: -20vh;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
一旦父容器尺寸发生变化,那么被居中的子元素大概率要改变其样式,才能保持垂直居中的状态。其中,子元素的定位属性和外边界属性都是相对于包含块进行计算的,此例中,包含块由父元素生成,对应父元素的尺寸。
哦,我想起一个事情需要告诉你,有时候指定一些属性会强制覆盖其他的属性,有些不接受再次覆盖,有些仍可被覆盖。
比如我们这里,指定了绝对定位,那么它的显示类型被永久地覆盖为 block
,可以设置尺寸等。同时,z-index
也相应地被设置为了 0,这个是可以覆盖的。
再如弹性容器覆盖弹性盒子的显示类型不可覆盖,以及使其宽度默认必须大于可容纳内容的最小宽度,后者可覆盖。
2. 利用 CSS 函数 translate
在“指定绝对位置”的基础上,我们不再借助像素值这种绝对单位的值进行调整,改之以平移。
transform
中的 translate*
CSS 函数系列允许我们相对于元素自身进行偏移,而不是包含块。当然,你也可以使用 matrix*
系列函数,这可以使用变换矩阵一举囊括所有变换。
这个方法看着非常美好,即使父元素大小不确定也可以生效,但代价是子元素再次需要使用 transform
时,必须添加已有的偏移函数,或是通过手动添加,或是通过 CSS 变量实现此需要。
2022年以来较新浏览器开始支持的 translate
属性也可以类似地起到相似效果,而不占用 transform
。
.parent {
position: relative;
width: 100vw;
height: 100vh;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
3. 利用盒模型的计算规则
我们对元素进行水平居中时可以在子元素尺寸指定时,利用 margin: 0 auto
指定水平两侧外边距为 auto
,那有没有可能借助此属性,在垂直上使得子元素居中呢?
是可以的。
默认情况下,流布局中仅有外部显示类型为块级的元素才会具备此约束——水平方向上,left
、right
、margin
、padding
、以及内容盒的 width
相加值必须要与其包含块的宽度相等。这意味着,如果其中一个值为负值是合法的,那么其他值需要增加相应的值以满足此条件。反之亦然。如果所有值都具体指定,那么文本方向的最后一个属性(通常是 left
或 right
)将会被重置为 auto
。在此场景下,只要元素指定 auto
,便可以将剩余的值(可负)相等地划分给该元素。当然,前提是支持 auto
值。
我们有两种方法可以启用垂直方向的此约束。
- 指定元素为绝对定位元素。
- 使父元素为弹性容器,子元素相应成为弹性元素,规定垂直方向对应的轴以
strench
排布内容。 - 使父元素为网格容器,子元素相应成为网格元素,规定垂直方向对应的轴以
strench
排布内容。
当然,方法2和3再为了居中使用 margin 就多此一举了。不过,了解这个特性这可以帮助我们预测样式可能出现的问题。
方法1:指定元素为绝对定位元素
方法1需要指定元素对应方向——我们这里是上和下方向——以使外边距可以充分填充内容。绝对定位并指定各方向为0的情况很常见,过去没有 vh/vw 时,我们常常用这个方法去让元素占据整个视口,但你是否知道此时垂直方向再加上 margin: auto 0
可以让元素外边距划分剩余空间,从而居中元素呢?
.parent {
position: relative;
width: 100vw;
height: 100vh;
}
.child {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
方法2:使父元素为弹性容器
我猜,你可能意外发现过这个方法,但没有细想。打开开发者面板,选中子元素看一下计算后的边距吧!你会发现,无论你怎么调整父元素的弹性盒布局相关属性,子元素永远是居中的。这也太奇怪了。
其实,生成弹性盒上下文后,内部相关弹性属性计算需要先考虑元素原本的尺寸,随后再将多余的空间按照指定的规则分配,或是缺少的空间按照指定的规则从各子元素扣除。但 margin: auto
在这之前根据弹性盒元素的基准 flex-biasis
被计算了,因此,在此情况时,无论你怎么调整弹性盒关于内容分布的其他属性,内部总是没有多余/缺少的空间,不会产生变化。
.parent {
display: flex;
width: 100vw;
height: 100vh;
}
.child {
margin: auto;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
给出弹性盒布局中元素计算的公式: 如果每个弹性元素
方法3:使父元素为网格容器
类似于方法2,网格布局也可以这样操作。
.parent {
display: grid;
width: 100vw;
height: 100vh;
}
.child {
margin: auto;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
4. 利用 vertical-align
这也是一个古早时期就用于垂直居中的技巧。这个技巧按照如何处理其余要素,主要分为两种:
- 不修父元素显示类型,设置父和子
line-height
。 - 修改父元素显示类型为
table-cell
。
方法1:不修父元素显示类型,设置父和子 line-height
方法1使得行内元素所在行框的高度变化,变至与包含块相同的高度,从视觉上达到居中的效果。因为 line-height
的百分比是相对其包含块计算的,所以我们需要指定其他类型的值。你也必须额外指定子元素的 line-height
,以改变其行框的高度大致等同于其高度用以对齐。不要忘了这一点,尽管有时候可能不需要额外地调整。
.parent {
width: 100vw;
height: 100vh;
line-height: 100vh;
text-align: center;
}
.child {
display: inline-block;
margin: 0 auto;
width: 40vw;
height: 40vh;
line-height: 40vh;
background-color: black;
}
在沙盒中尝试效果
发现突出的方块了吗?那是用于对比子元素是否居中的辅助提示,如果完全覆盖,则证明真正地垂直居中了。看来设置行高没有让子元素完全居中。
为什么会出现这种情况呢?这是因为,块级元素内部为每一行生成行框,而行框内部的行内元素默认情况会与此行框的基线位置对齐,即 vertical-align
默认为 baseline
。
如果是文字节点(匿名非置换行内元素)或非置换行内元素,使用其行内框基线与行框基线对齐。如果是行内置换元素,规范未作说明,因此以实现为准,Chrome 会将默认以高度的一半为基准与基线对齐。
这也是我们布局图片和文字时,它们垂直上默认总是不对齐的原因。
那么,如果我们指定 vertical-align
为 middle
呢?这回都全居中了,总该覆盖上了吧。
.parent {
width: 100vw;
height: 100vh;
line-height: 100vh;
text-align: center;
}
.child {
display: inline-block;
margin: 0 auto;
width: 40vw;
height: 40vh;
line-height: 40vh;
vertical-align: middle;
background-color: black;
}
在沙盒中尝试效果
还是有一点点冒出来了,不过问题不大!如果没有像素级还原的要求,再加上这个突出相对整个父元素高度较小,其实就无所谓了。这个凸出部分有多小呢?指定行内元素 vertical-align: middle
时,其将把自己的垂直方向一半的位置与父元素基线往上半个 x 高度的位置对齐。这半个 x 高度,就是凸出部分的高度了。(x 高度,即 0.5ex,为字母 x 在该字体此时对应字号的高度,一般浏览器中实现为当前字号高度的一半)
所以,此方法中,我们常常需要额外加上负值外边距或位置偏移以精准地进行居中。
你可能也注意到了,此方法必须设置子元素外部显示类型为行内,即 inline
或 inline-block
。因此,子元素的块级行宽约束将失效,不再可以使用 margin: auto 0
进行水平居中。相应地,我们因此可以借助设置父元素 text-align: center
以居中子元素。
这是一个缺点相当明显的方法。现在我们的行高和父元素生成的容纳块高度一致,如果居中的是文本,在换行时甚至可能出现两倍高的子元素,出现非预期的样式。
另一个更大的负面影响是,如果我们子元素内部嵌套了其他元素,这些元素的 line-height
全部会继承我们此处的设置,要提高警惕。
方法2:修改父元素显示类型为 table-cell
。
vertical-align
可以应用于行内元素和表格单元格元素,对于不同显示类型的容器,其各值的对齐基准都不同,甚至像我们刚才提到的,行内非置换和置换元素部分值也会有差异。这在 MDN 文档的相应页面进行了详细的说明。
对于显示类型为 table-cell
的元素,垂直居中其子元素就要轻松得多了。因为设置其子元素 vertical-align: middle
是真的将子元素的垂直中心与父元素的中心对齐。
.parent {
display: table-cell;
text-align: center;
vertical-align: middle;
width: 100vw;
height: 100vh;
}
.child {
display: block;
margin: 0 auto;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
使用此方法,需要考虑父元素的显示类型是否可以正常覆盖。假如我们父元素处于一个弹性盒布局或是网格布局中,那么其显示类型被锁定,是无法设置的。另外,如果你需要指定内部的格式上下文,如设置弹性盒格式上下文,那就与 table-cell
冲突了。
5. 利用弹性盒格式上下文
这也是现代前端开发中最常用且最好用的一种居中方法。
当主轴方向为水平方向时,即 flex-direction
为 row
或 row-reverse
, 仅需要设置交叉轴方向每个弹性盒内的内容分布。一是,设置父元素的 align-items
为 center
。二可以直接设置子元素的 align-self
为 center
。
当主轴方向为垂直方向时,即 flex-direction
为 column
或 column-reverse
时,仅需要设置主轴方向弹性盒的内容分布。可使父元素的 justify-content
为 center
。此时你可能会考虑子元素的 justify-self: center
能否类似地达到同样效果,但在弹性盒子布局中,该属性是被忽略的。
/* 行居中 */
.parent {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
}
.child {
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
/* 行居中 */
.parent {
display: flex;
justify-content: center;
width: 100vw;
height: 100vh;
}
.child {
align-self: center;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
/* 列居中 */
.parent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
}
.child {
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
/* 列居中 */
.parent {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
}
.child {
justify-self: center;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
6. 利用网格格式上下文
相对于弹性盒布局的一维,网格布局又额外增加了一个维度,能做的事情就更多了!
方法1:align-items
或是 align-self
首先,我们可以利用刚才提及的网格布局的那些属性,做垂直居中这件事。
在网格布局中,justify-content
设置网格容器行内轴中各个格子和周围空间的分配,justify-self
设置这之中行轴方向每个格子内的分布。
在网格布局中,align-content
设置网格容器中块级元素的块向轴和周围空间的分配,align-self
设置这之中行轴方向每个格子内的分布。
此外,终于在网格布局中,justify-self
生效了。尽管不能交换网格布局的轴,用它进行垂直居中,但我们仍可以用它做水平居中,可喜可贺!
题外话:你可能在 MDN 文档或是草案规范中发现 justify-self
可以用来调整区块布局或是绝对定位的位置,但现在仍然在草案阶段(截止2024年7月10日),还是没有实现的。
方法2:利用网格布局特性
或用网格布局的特性,我们可以将元素放置到划分好的指定位置,像是:
.parent {
display: grid;
grid-template-rows: repeat(3, 1fr);
grid-template-column: repeat(3, 1fr);
width: 100vw;
height: 100vh;
}
.child {
grid-column: 2;
grid-row: 2;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
7. 利用伪元素
这一类方法用于垂直居中就相对比较笨重了,但是个很好的思路。
方法1:直接插入固定高度的伪元素
如果子元素高度调整,那么伪元素也必须相应调整了。可以通过 CSS 变量克服此缺点。
.parent {
width: 100vw;
height: 100vh;
}
.parent::before,
.parent::after {
display: block;
height: 30vh;
content: '';
}
.child {
margin: 0 auto;
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
方法2:借助弹性盒布局插入自动缩放的伪元素
有点多此一举了,但其实也不是不能这样做。
.parent {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
}
.parent::before,
.parent::after {
flex: 1; /* 1 1 0 */
content: '';
}
.child {
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
方法3:借助网格布局插入自动缩放的伪元素
自然,我们如果划分好网格区域,也可以通过追加伪元素达到指定行列放置的效果。
.parent {
display: grid;
grid-template-rows: repeat(3, 1fr);
width: 100vw;
height: 100vh;
}
.parent::before,
.parent::after {
content: "";
}
.child {
justify-self: center; /* 水平居中 */
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
方法4:弹性盒布局多行伪元素
如果我们不将弹性盒的主轴方向进行改变,同样也可以通过换行实现居中。非常明显的缺点是类似于方法1,也需要指定伪元素的高度。同样可以通过 CSS 变量克服。
这里使用了 align-content: center
,使得我们交叉轴上的每一盒子内交叉轴方向可以居中地分布。这里使子元素水平居中了。
.parent {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 100vw;
height: 100vh;
}
.parent::before,
.parent::after {
flex: 1 0 100%; /* 1 1 0 */
height: 30vh;
content: '';
}
.child {
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
方法5:网格布局多行伪元素
自然,我们如果划分好网格区域,也可以通过追加伪元素达到指定行列放置的效果。
如果不设置,那么默认每个元素占据其中的一行。你可以选择使用 align-content
来调整块向轴如何分布(即纵向分布),或是为伪元素指定高度以矫正子元素的位置。
.parent {
display: grid;
flex-wrap: wrap;
justify-content: center; /* 水平居中 */
align-content: center;
width: 100vw;
height: 100vh;
}
.parent::before,
.parent::after {
content: '';
}
.child {
width: 40vw;
height: 40vh;
background-color: black;
}
在沙盒中尝试效果
结语
我们总结了数十种利用 CSS 对元素进行居中的方法,对部分 CSS 的特性进行了回顾,现在,再遇到什么居中产生的异常情况应该难不倒你了。但要记住的是,除了纯 CSS 实现,我们也可以通过 JavaScript 计算去做位置的调整,这也是一个非常好的方法。
参考资料
[1]: [美]Eric A.Meyer. CSS 权威指南[M]. 安道. 北京: 中国电力出版社, 2019: 262-315, 564-734
[2]: W3C. CSS Display Module Level 3 [DB/OL]. 参考链接. 2023-11-22
[3]: MDN Web Docs Community. vertical-align [DB/OL]. 参考链接. 2024-06-22
[4]: MDN Web Docs Community. display [DB/OL]. 参考链接. 2024-06-22