十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CSS水平垂直居中全方案解析:从传统到Flex与Grid的核心原理

CSS水平垂直居中全方案解析:从传统到Flex与Grid的核心原理 这个题目我太熟了。面试的时候十次有八次会被问到水平垂直居中平时群里也经常有人贴代码问为什么没居中。说实话这题看着简单但它考察的东西一点都不浅盒模型、文档流、定位机制、Flex 和 Grid 布局的核心逻辑一个居中能全部串起来。而且关键在于同一个需求在不同场景下答案完全不一样——文本、图片、宽高固定的盒子、宽高未知的弹层各有各的最优解没有一套方案能通吃所有情况。我这篇就把这些年我实际用过的方案完整过一遍从最传统的 text-align、line-height 一直讲到 Grid 的一行代码居中。每种方案我都会说清楚它的核心思路、适用场景和我踩过的坑顺便把面试官常追问的那些为什么也一并讲透。1. 先搞明白为什么垂直居中比水平居中麻烦很多人一开始学 CSS 就发现一个现象水平居中很容易垂直居中怎么弄都不对劲。这不是你技术不行是 CSS 这套布局模型本身就偏向水平方向垂直方向没有任何一个天然的居中属性。1.1 水平居中的天然优势先看文字。文本在水平方向上的排列是从左往右填满一整行所以只要给父容器加一个text-align: center行内元素天然就居中了。再看块级元素一个定宽的块在水平方向上有剩余的空白空间margin-left: auto和margin-right: auto会自动把这部分剩余空间平分给左右两侧块也就居中了。这两个机制其实都在做同一件事把剩余空间合理地分配到两侧。水平方向之所以这么顺手是因为 CSS 的文档流天然有一个固定的参考线——每一行都有一个宽度可以计算剩余空间可以明确地被分配掉。1.2 垂直方向没有分配剩余空间的默认机制垂直方向就不一样了。文档流是从上往下依次排列的元素的高度由内容撑开默认情况下父容器根本不知道我应该把剩余空间放到哪里去。除非你告诉它一个明确的高度否则margin: auto在垂直方向上是不生效的。注意块级元素的margin-top: auto和margin-bottom: auto在实际布局中的计算结果是 0这是规范里明确写的。这也是为什么只靠 margin 无法让普通块元素垂直居中的根本原因。所以垂直居中难这件事本质上是文档流模型决定的。后面讲的所有方案核心思路其实就是在绕过这个限制——要么把元素拉到文档流之外要么建立一个全新的布局上下文要么模拟表格的单元格行为。2. 文本与行内元素的居中三板斧如果只是要居中一段文本或者几个行内元素其实不需要上什么大招老老实实用文本相关的属性就能搞定。这类方案看着简单但一直是很多小需求的主力军。2.1 text-align 配合 line-height 实现单行文本居中最常见的场景一个导航栏按钮或者一个高度固定的标题条里面只有一行文字要求水平和垂直都居中。最简单的方式就是水平方向用text-align: center垂直方向让行高等于容器高度.banner { width: 400px; height: 60px; text-align: center; line-height: 60px; background: #f0f4f8; }为什么 line-height 能居中因为 line-height 在渲染时会把额外的行高平均分配到文字行框的上下两侧。当行高和容器高度相等时文字行框的高度刚好占满容器上下各多出来的半行高也就正好相等看起来就是垂直居中。这个方案有一个严格的限制它只对单行文本有效。如果文字换行变成两行两行文本的总高度会超过一个 line-height 的高度垂直居中直接失效。另外如果容器有 padding那 line-height 和容器高度的关系就要重新计算会变得很麻烦。实操心得我自己做这种单行居中时更习惯给容器写line-height: 1或具体的像素值然后配合 padding 来控制整体高度尽量避免直接用 line-height 硬怼容器高度。因为 line-height 一旦被撑到 60px后续如果要在里面加图标或稍大一点的字体行高会和内容打架排查起来非常别扭。2.2 用 table-cell 模拟表格单元格居中表格单元格td天然支持vertical-align: middle这是 HTML 里最早就有的垂直居中能力。CSS 里可以通过display: table和display: table-cell把普通元素伪装成表格结构从而借用这个能力.parent { display: table; width: 400px; height: 300px; } .child { display: table-cell; vertical-align: middle; text-align: center; }这种方式兼容性极好IE8 时代就在用了直到今天仍然是一个稳定的兜底方案。它能让任何内容单行文本、多行文本、图片甚至一个块级子容器在已知高度的父容器里完美居中。但 table-cell 有两个明显的问题。第一它改变了元素的 display 值父容器设置成 table 之后margin 无法再用相邻元素的间距需要用其他方式处理。第二table 布局的宽度分配有自己的算法子元素的宽度有时候不会完全听你的。所以这个方案更适合做内部结构简单、只需要整体居中的场景不太适合做复杂布局的一部分。2.3 inline-block 和伪元素的配合方案这个方案属于 table-cell 的变体核心思路是在父元素里加一个高度跟父容器一样、宽度为 0 的辅助行内元素用它把行框的高度撑满然后让真正的内容元素通过vertical-align: middle在这个撑高的行框里居中.parent { text-align: center; } .parent::after { content: ; display: inline-block; height: 100%; vertical-align: middle; } .child { display: inline-block; vertical-align: middle; }这个方案不需要父元素设置display: table对整体布局的干扰更小兼容性也非常好。它是早期不能随便用 flex 时我最常用的垂直居中方案之一。不过这个方案的理解成本稍微高一点因为它依赖的是一个隐形元素来撑高行框。我第一次看这段代码的时候也愣了半天后来才想明白 vertical-align 的参照对象是行框而行框的高度由所有行内元素中最高那个决定——所以先塞一个 height: 100% 的元素进去行框就被撑起来了。3. position 绝对定位的三类实现当你要居中的是一个明确的块级元素比如一个弹窗、一张卡片而且它的宽高是可控的那么绝对定位是绕不开的话题。绝对定位最大的好处是让元素脱离文档流不再受从上往下排列的规则约束可以精确地出现在任意位置。3.1 负 margin 方案最经典也最笨拙这个方案的前提是你必须知道元素的具体宽高.parent { position: relative; } .child { position: absolute; left: 50%; top: 50%; width: 200px; height: 100px; margin-left: -100px; margin-top: -50px; }原理很好理解先把子元素的左上角放到父容器的正中心left: 50% 和 top: 50% 都是以父容器宽高为基准的百分比再通过负 margin 把子元素向左、向上移动自身宽高的一半这样子元素的中心就和父容器的中心重合了。这个方案的缺点是显而易见的宽高一旦变化margin 值也要跟着改。如果内容是动态渲染的宽高根本没法预先知道这方案基本没法用。它的存在意义更多是历史性的——在 transform 和 flex 普及之前这已经是相对可靠的做法了。3.2 transform 偏移方案不再依赖宽高transform 方案的写法几乎和负 margin 一样唯一区别是把移动自身宽高一半这个动作交给 transform 完成.child { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }这里的关键区别在于translate的百分比是相对于元素自身的宽高来计算的而不是父容器。也就是说你不需要事先知道子元素的宽高它自己会往左上角退半个身位。这个特性让它成为很长一段时间里未知宽高元素居中的首选方案。这个方案用着舒服但也别忘了它有一个副作用transform会给元素创建新的层叠上下文还会让这个元素变成它内部position: fixed后代的包含块。说人话就是如果这个居中的元素里面还有一个 fixed 定位的子元素那这个子元素将不再相对于浏览器视口定位而是相对于这个有 transform 的父元素定位。我在实际开发中因为这个 bug 排查过很长时间等会儿在问题排查部分细说。3.3 四向归零加 margin: auto 的巧思还有一个相对冷门但原理很有意思的方案写法是这样.child { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 200px; height: 100px; margin: auto; }当绝对定位元素的上下左右四个方向都被设置为 0 时元素会被拉伸到父容器的边界。但如果同时指定了固定的宽高元素就不会被拉伸而是停在中央。这时候margin: auto会自动分配四个方向上的剩余空间上下左右各分到一半元素就居中偏好了。这个方案的兼容性写法是top: 0; right: 0; bottom: 0; left: 0;新一点的写法可以用inset: 0一次搞定。它和负 margin 方案一样要求宽高固定但好处是不需要手动计算负 margin宽高调整时 margin 会自动跟着变。我个人觉得如果需要兼容老浏览器、又不想用 transform这个方案比负 margin 更省心。4. Flexbox 与 Grid现代布局里居中只是基本功为什么说 Flex 和 Grid 是降维打击因为前面所有方案的核心矛盾都是CSS 不知道剩余空间该怎么分而 Flex 和 Grid 本来就是为分配剩余空间而设计的。居中这件事在它们看来只是一个最最简单的默认行为。4.1 Flex 容器一行代码搞定水平和垂直居中Flex 做法可以说是目前应用最广的居中方案.parent { display: flex; justify-content: center; align-items: center; }这两行代码背后的逻辑值得展开讲一下。Flex 布局里有两条轴justify-content控制的是主轴方向上的对齐align-items控制的是交叉轴方向上的对齐。在不修改flex-direction的情况下主轴是水平方向交叉轴是垂直方向所以刚好一个管水平、一个管垂直。如果你把flex-direction改成column那主轴就变成了垂直方向justify-content: center管的就是垂直居中align-items: center反而管水平居中了。很多人改了主轴方向之后发现居中失效十有八九就是忘了这两个属性的作用方向会跟着主轴变化。另外还有一个容易被忽略的点align-items的默认值是stretch不是flex-start。也就是说子元素默认会被拉伸填满交叉轴方向的空间。如果你靠align-items: center居中了一个图片图片没有被拉伸那是正常的但如果你希望子元素高度自适应内容、又只做水平居中记得把align-items显式设置成center或flex-start。4.2 Flex 子项 margin: auto 的巧妙用法在 Flex 容器里子元素设置margin: auto可以吸收剩余空间。比如下面这段代码不需要在父容器上写任何对齐属性子元素自己就能居中.parent { display: flex; } .child { margin: auto; }这个写法的可读性其实比justify-content: center; align-items: center;更高一点因为它直接把我要居中这个意图挂在了需要居中的那个元素身上。尤其当一个容器里有多个元素你只想让其中一个居中、其他元素保持在原来位置时margin: auto的优势就很明显——子项级别的控制粒度更细。实操心得Flex 方案我去掉了百分比的坑也没有 line-height 的单行限制宽高未知也没关系确实是最省心的现代方案。如果项目不需要兼容 IE10 以下的浏览器我基本默认都用它。4.3 Grid 布局一行代码的极致简洁Grid 方案就更简洁了.parent { display: grid; place-items: center; }place-items是justify-items和align-items的简写分别控制网格项在水平方向和垂直方向上的对齐。center就是居中。整个方案只需要一行 CSS是目前代码量最少的居中方案。这里有一个特别容易搞混的点place-items和place-content看起来很像但作用完全不同。justify-items和align-items控制的是网格项在它自己的网格区域内的对齐方式justify-content和align-content控制的则是整个网格轨道在网格容器内的排列方式。如果你把网格轨道设置成三列固定宽度这时候place-content: center能让这三列整体居中但place-items并不会移动这一组列的位置它只负责每个格子内部内容的对齐。理解了这层区别这两个属性基本就不会用混了。Grid 方案在支持 Grid 的浏览器里都工作得很好代码也最干净。唯一要斟酌的是团队里其他人对 Grid 的熟悉程度以及项目是否要兼容老的浏览器环境。如果这两点都没问题Grid 完全可以成为你首选的标准答案。5. 方案选型什么场景用什么以及面试怎么答看完了这么多方案真正的问题是实际开发里到底该用哪个这其实没有一个绝对正确的答案更多是看你的项目环境、目标浏览器和场景复杂度。我这里给出一个我自己平时判断的参考方向。5.1 常用方案对比速查表方案是否需要知道宽高兼容性代码量主要缺点text-align line-height不需要单行文本极好极少仅限单行文本table-cell需要极好中等改变 display对布局有影响inline-block 伪元素不需要极好中等理解成本稍高absolute 负 margin必须知道极好少宽高变化要同步改 marginabsolute transform不需要良好少transform 有层叠上下文副作用absolute inset margin auto必须知道良好少老浏览器需要写四向 0Flexbox不需要良好少老 IE 需要前缀处理Grid不需要现代浏览器一行对老浏览器不友好我这里说的兼容性是结合目前主流业务开发还会碰到的浏览器环境来评估的。如果项目只跑在最新版浏览器上那兼容性一栏可以全部划掉无脑选 Flex 或 Grid 就行如果偶尔还要兼容老版本浏览器table-cell 和 absolute 那一排依然是可靠的兜底。5.2 真实项目里我的选型逻辑我自己的习惯是分三层考虑。界面布局级别的居中比如整页的引导页、登录页中央卡片我一般会用 Flex因为它的兼容性好代码可读性也高团队里的新人看到justify-content: center; align-items: center;基本不用问就知道在干什么。如果项目里已经在用 Grid 做整体布局那顺手用place-items: center也完全没问题。小范围内的局部居中比如一个按钮里的文本、一个图标和文字的搭配我倾向于先想清楚是文本居中还是盒子居中。文本居中直接用 text-align 和 line-height 就行盒子居中用 Flex 或 inline-block。不要动不动就开一个 flex局部居中也上 flex 容易把嵌套层级搞得很深。对已知宽高的弹窗类组件我反而更偏好 absolute 加 margin auto 的方案。因为弹窗一般需要脱离文档流用 position 定位是顺理成章的。而 transform 方案虽然代码更短但那个层叠上下文和包含块的副作用在弹窗这种层级复杂的组件里很容易惹出麻烦。面试里被问到这道题的时候你不需要把八个方案全背一遍但至少要把如何水平居中、如何垂直居中的基本逻辑和代码写出来同时能说出每个方案的适用条件和局限。如果把transform为什么不需要知道宽高、flex-direction改变了justify-content和align-items的作用方向这几点讲清楚面试官基本就能确认你是真的理解布局而不是背答案。5.3 不只是居中还要考虑响应式一个容易被忽略的现实问题是很多页面的容器高度不是固定的而是由内容撑开的。这种情况下所有知道父容器高度的方案都会失效table-cell、line-height 都用不上Flex 和 Grid 反而是最能扛的因为它们天然不依赖固定的高度值。比如一个卡片列表卡片里文字有多有少要求图片垂直居中文字多的时候也不溢出。这种场景用 Flex 的align-items: center就非常合适因为父卡片的高度本来就是自适应的Flex 会在当前高度下完成居中。table-cell 虽然也能做到但高度不固定时它的行高表现有时候会有轻微偏移调试起来会麻烦一些。实操心得我后来做居中方案时会先问自己一个问题父容器和子元素的尺寸是确定的还是动态的如果是动态的直接跳过 table-cell、line-height、负 margin 这些方案往 Flex 和 Grid 走能节省大量调试时间。6. 实战中踩过的坑与排查速查表最后这部分是真正的干货区。这些都是我在实际项目里踩过、排查过、甚至被测试同事抓到过的 bug整理成一个速查表方便你遇到问题直接对照。6.1 绝对定位后元素尺寸变化的坑用绝对定位做居中时子元素的尺寸表现和普通文档流完全不同。如果你只写了left: 50%; top: 50%;但没写宽高子元素会收缩成内容的宽度配合 transform 居中时看起来是居中了其实它的定位原点已经不在你预期的地方。如果你用inset: 0这类写法把四向都设成 0子元素又会被拉伸填满父容器这时候不设宽高你根本看不出它在居中。排查方法很简单打开浏览器开发者工具看看子元素的实际盒模型尺寸和你预期是否一致。如果尺寸不对先把宽度、高度或内容撑起来再去看定位。6.2 transform 带来的 fixed 定位变故这是我被坑得最惨的一次。有一个居中的弹窗组件我在它的父层上用了transform: translate(-50%, -50%)来居中结果弹窗内部的关闭按钮和下拉菜单全部定位错乱。查了很久才发现transform会改变元素的定位参考系让内部position: fixed的元素不再相对于浏览器视口定位而是相对于这个 transform 元素。从那以后只要组件里存在 fixed 定位的子元素我基本不用 transform 方案做居中改成 Flex 或直接用绝对定位加 margin auto。如果你的项目里已经遇到了这个问题最快的验证方法就是把 transform 那行临时注释掉看看 fixed 元素是否恢复。如果恢复了就是这个原因没跑了。6.3 flex 布局中图片或子元素被拉伸变形Flex 容器的align-items默认是stretch。如果你没有显式设置align-items: center但设置了justify-content: center子元素在水平方向居中了却在垂直方向被拉伸到父容器高度。这会直接导致部分图片被变形拉伸或者子元素背景色异常撑满。一个隐藏更深的情况是就算你设置了align-items: center如果子元素自身有flex: 1之类的伸缩属性它仍然会尽可能填满剩余空间。所以排查居中问题时看到子元素尺寸不对劲先检查父容器的align-items和子元素的flex-grow是否互相冲突。6.4 文本居中失效的常见原因用line-height做单行居中失效通常是两个原因一是文本实际换行了行高计算方式和单行不一样二是容器有 padding 或 border导致 line-height 和内容区域的实际高度对不上。另外vertical-align只对inline和inline-block元素生效如果你给一个display: block的块元素设置vertical-align: middle是没有任何效果的。6.5 常见问题速查表现象可能原因解决方案使用 line-height 后多行文本没居中line-height 只对单行文本可靠改用 Flex 或 table-cell绝对定位后元素不见了没有设置宽高内容为空时尺寸为 0给元素设置宽高或添加内容transform 方案后内部 fixed 元素错位transform 创建了新的包含块更换为 Flex 或 absolutemargin autoflex 子元素被拉伸align-items 默认值为 stretch显式设置 align-items: center使用 margin auto 无法垂直居中在普通文档流中纵向 auto 计算为 0改用 Flex 容器或绝对定位table-cell 方案中子元素宽度异常表格布局的宽度分配规则不同给 table 设置固定宽度或改用 FlexGrid 设置了 place-content 但没居中place-content 控制网格轨道不控制项改写成 place-items: center6.6 最后分享一个小技巧如果你在做一些偏工具的页面、对性能要求比较高可以用一个不常见的语法来自我检查方案是否合理Flex 容器里把子元素写成margin: auto其实比在父容器上写两个对齐属性更贴近最小改动原则。因为margin: auto不依赖父容器的任何属性也不会被子元素的伸缩属性影响代码量也更少。这个写法我在好几处局部居中里都用了稳定且省事。按照我自己的体会水平垂直居中这道题之所以会被反复问是因为它真的考察了一个人能不能从布局机制的层面去理解 CSS而不是背代码。你把原理搞懂了剩下的无非是选择最合适的那个方案而已。
返回列表