ARTICLE DETAIL

资讯详情

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

水平垂直居中的终极指南:从原理到选型的判断逻辑

水平垂直居中的终极指南:从原理到选型的判断逻辑 水平垂直居中一个被问烂了的问题。我见过很多前端新人背了一大堆方案却不知道怎么选flex 能居中absolute transform 也能居中table-cell 也能居中可一旦换个场景——弹窗宽度不固定、父容器有滚动条、子元素里还套着多层 transform——原有方案说失效就失效。这篇文章按我实际项目里沉淀下来的思路来写不打算把每个方案的代码再抄一遍而是把最关键的判断逻辑讲透元素尺寸是否已知、对齐的参考系是谁、周围有没有滚动和嵌套层级的干扰。这三个问题想明白水平垂直居中就不再是个需要背的面试题而是一眼能定方案的常规操作。适合正在整理 CSS 基础、或者做项目时反复被居中问题折磨的开发者尤其是需要兼容老浏览器的场景后面几节会特别有用。1. 先把问题拆清楚居中不是一个方案而是两个变量的组合很多人之所以觉得水平垂直居中乱是因为把所有情况混成一锅粥了。实际上判断路径非常简单就两个变量子元素尺寸已知还是未知以及你要相对谁居中。这两个变量一确定方案池直接砍掉一半。1.1 尺寸已知与尺寸未知是第一个岔路口如果子元素的宽高是写死的比如一个 300 × 200 的弹窗卡片那几乎什么方案都能做从最古老的margin负值到最新的place-items都能跑。但更多时候子元素尺寸是内容撑出来的比如一段文案、一张不确定宽度的图片这时候margin负值方案直接出局因为你没法写margin-left: -150px——你根本不知道 150 这个数字是多少。还有第三种情况尺寸在运行时会变比如文案从确定变成确定要删除这条数据吗或者内容来自接口。这种情况比未知更麻烦因为就算你打开浏览器发现当前能用下一次数据一变可能就歪了。所以我的经验是只要宽高可能变化一律按未知尺寸处理。1.2 对齐参考系究竟让元素居中于谁第二个问题是参考系。你的元素需要居中于整个视口比如全局 loading 遮罩、弹窗背景层某个相对定位的父容器比如卡片里的 loading 动图一个滚动区域的内容区比如列表加载更多的提示一个栅格单元格table 布局时代最常见的需求不同参考系下absolute的寻找逻辑完全不同它找的是最近的position非static的祖先。如果你心里想的参考系是视口代码里写的是absolute但祖先链上正好有个transform的元素那实际效果会跑偏到莫名其妙的地方。这一块后面会有专门一节讲先有个概念选方案前先明确参考系。2. 三种主流方案深拆代码谁都会写原理才是分水岭这一节讲三个真正扛得住生产环境的方案。每个方案我都会写原理、代码和失效边界不搞花活全是实战验证过的。2.1 flex 双轴居中现代布局的默认首选.container { display: flex; justify-content: center; align-items: center; }这三行是绝大多数场景的地板答案。原理要讲清楚flex 容器有主轴和交叉轴justify-content: center让主轴上居中align-items: center让交叉轴上居中。默认flex-direction: row时主轴是水平方向交叉轴是垂直方向所以这两个属性正好压住水平和垂直两个维度。我在项目中用 flex 居中时有三个默认设置必须看一眼align-items的默认值是stretch而不是center。如果我只想水平居中但垂直方向希望子元素撑满容器我反而要依赖 stretch 这个默认值这时只要justify-content: center就够了。很多人上来就写align-items: center结果发现子元素高度没有自动撑开还纳闷为什么。这不是 bug是 flex 的语义本就如此。多行 flex 容器里align-content是另一个独立属性。当子元素换行后垂直方向的对齐由align-content控制而不是align-items。比如一排十个卡片折成了两行你想让这两行在垂直方向居中必须写align-content: center只写align-items: center是不生效的。这个坑在写标签列表、图片墙时非常常见。子元素的min-height: auto也容易坑人。flex 子项默认不能小于其内容的固有尺寸所以当一个很高的内容塞进 flex 居中容器时子项不会老老实实压缩而是把容器撑破视觉上就溢出且偏上。解决方法是给子项设置min-height: 0或overflow: hidden。flex 是首选不是因为新是因为语义最直白而且对未知尺寸的子元素最友好。但如果你需要兼容 IE10 及更早版本justify-content和align-items需要加-ms-前缀而且老 IE 对某几个取值支持不全这种情况下我会直接用下面的绝对定位方案。2.2 grid 的 place-items一行代码进去容器直接解决问题.container { display: grid; place-items: center; }place-items是align-items和justify-items的简写。在 grid 容器里写place-items: center子元素会在网格区域内同时水平垂直居中。这个方案最大的好处是写在容器上子元素自己不需要任何样式比 flex 需要两个属性还要更简洁。grid 有一个 flex 给不了的特性如果容器是个多行多列的网格某个子元素要用grid-column和grid-row放到指定格子而格子距离中心有偏差这时候你可以用place-self: center让这个特定的子元素在自己所在的网格区域内居中而其他元素保持原样。这在做复杂仪表盘布局时很实用。还有一个高频场景单行文本图标组合。一个按钮里同时有图标和文字想让整体水平垂直居中用 grid place-items: center直接在按钮内摆个单一网格效果非常干净。我经常在工具栏、图标按钮这类小组件上这么干。place-items对兼容性要求稍高Chrome 57、Firefox 45、Safari 10.1。如果你面向的是现代浏览器项目这个方案几乎是最省事的。老项目我会看是否用 grid 布局整体迁移如果整体没迁就不单独为了居中引入 grid。2.3 绝对定位组合技transform 与 margin 的本质差异绝对定位家族里有一对兄弟表面上都能居中原理完全不同/* 方式 Atransform 平移 */ .container { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 方式 Bmargin auto 分配剩余空间 */ .container { position: relative; } .child { position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; }先说方式 A。top: 50%会让子元素的左边缘对齐到容器一半的位置transform: translate(-50%, -50%)再让它往左往回挪自身尺寸的一半。关键点transform的百分比是相对元素自身尺寸计算的不是相对容器。所以不管子元素多大多小这一套都能居中。方式 B 的原理更冷门但非常值得理解当绝对定位元素四个方向都设了0而元素又有确定的宽高或内容撑出尺寸时margin: auto会把剩余空间平分给上下左右的外边距从而把元素推到正中心。这是 margin auto 在普通流里做不到的因为普通流里垂直方向的 auto 是无效的只有绝对定位加四边 0 才能激活它。我在实战中很少用方式 A因为transform在子元素自身带animation时容易被覆盖。比如想做一个循环浮动效果keyframes里写transform: translateY(-10px)一执行就把居中的translate(-50%, -50%)覆盖了弹窗当场歪掉。这种场景我会把方式 B 作为基础定位再在动画里只使用top或margin做偏移或者干脆在里层再包一个 div 专门做动画。方式 B 有一个前提元素需要确定尺寸。如果子元素宽高都没有margin: auto会把四个方向的剩余空间全分给 margin但元素宽度为 0看似居中实际上内容并没有被正确包裹。所以方式 B 我会搭配明确的高度或者让子元素width: fit-content来用。left: 0; right: 0; margin: auto还有一个经典场景让一个固定宽度的元素在水平方向居中同时垂直方向靠顶部固定距离。我写全局通知条、底部操作栏时经常这么组合比 flex 更精准不用考虑父容器有没有启用 flex 布局。3. 从旧时代那里借来的三个偏方table-cell、行高与 writing-mode有些方案在业务代码里不常出现但遇到特定场景时比 flex 和 grid 都好使。知道的方案越多遇到怪问题时越从容。3.1 table-cell老项目里最稳定的垂直居中方案.container { display: table; width: 100%; height: 400px; } .child { display: table-cell; vertical-align: middle; text-align: center; }原理很简单表格单元格天然具有垂直居中的特性vertical-align: middle在表格上下文里就是让单元格内容垂直居中。把容器模拟成表格子元素模拟成单元格就借来了这个能力。为什么现在不流行了因为它对内部结构有强约束display: table的容器里display: table-cell的兄弟元素会像表格列一样排布多个子元素会被强行放进同一行布局行为完全不同于 block。想在里面放绝对定位的遮罩层时表格的排版模型会让你怀疑人生。但它的优势是兼容性逆天IE6 都能跑而且高度不需要固定只要容器有确定高度单元格内容无论多少都保持垂直居中溢出时也只是滚动或裁剪。在必须兼容老内核浏览器、且布局简单的老项目里它是 flex 之外最可靠的选择。3.2 单行文本与多行文本line-height 的正确用法文本居中是最常被忽略的细分场景。让一行文字在固定高度容器里垂直居中最快的方法是让line-height等于容器高度.btn { height: 36px; line-height: 36px; text-align: center; }这里text-align: center处理水平方向line-height: 36px让文本行盒高度变成容器高度文字自然在垂直方向居中。这个方案的原理是文本行盒的上下空隙均分所以多行文本不能用多行文本会把行高叠加导致溢出。多行文本的正确解法给文本容器设display: flex; align-items: center或者用上面 table-cell 的vertical-align: middle让容器来撑不会因为换行而变形。还有一种做法是把line-height设成1.5之类的比例值让文本块自身居中但需要配合容器内边距微调不推荐作为首选。我自己的习惯按钮、标签、列表项这种单行内容直接用line-height方案段落、卡片描述这种多行内容一定用 flex 或 table-cell绝不在多行文本上硬套line-height: 容器高度。3.3 writing-mode 的逆向思维把水平方案翻转成垂直方案writing-mode可以把文档流从水平改为垂直。水平排列时text-align: center管水平翻转后text-align: center管垂直同理margin: 0 auto在这些模式下的方向也会跟着翻转。所以我可以用一套水平居中代码通过翻转来实现垂直居中.container { writing-mode: vertical-lr; text-align: center; }写成这样文本会在垂直方向上水平居中不对让我先把方向和习惯捋清楚writing-mode: vertical-lr会把块流向改为垂直水平方向的对齐需要依靠原来的垂直对齐属性。实际项目里这个方案几乎没有正面用途因为它会把布局方向彻底改掉副作用远大于收益。我之所以提它是因为它揭开了 CSS 一个核心真相水平和垂直在某些属性里只是主副轴的方向问题并没有本质区别。理解了这个你在看 flex 的主轴交叉轴、grid 的行列、writing-mode的方向切换时就不会觉得每个属性都是独立的而是一套坐标系规则下的不同表达。4. 从踩坑现场带回来的四份记录代码看起来对但就是不居中这一节全是实操中真正遇到的问题比单纯讲方案重要得多。4.1 容器高度塌陷所有纵向居中一起失效最常见的情况给容器写height: 100%但父级没有设置高度于是这个100%解析为 auto容器高度由内容撑起垂直方向的居中就没有参照物了。/* 这个写法在父级没有高度时垂直居中不会生效 */ .wrapper { height: 100%; display: flex; align-items: center; }排查办法打开 DevTools 看元素计算样式高度显示auto且实际高度等于内容高度时问题就出在这里。解决办法要么给父级链路上的每一层都设高度要么用100vh直接参考视口注意移动端地址栏收起时100vh会跳变建议配合100dvh或min-height: 100vh使用。我见过有人为了等父级层级不塌陷给 body、html、#root 全部设了height: 100%其实只用min-height: 100vh就能解决而且更灵活。这里的关键是优先让容器有明确高度再谈垂直居中。4.2 offsetParent 陷阱absolute 参照的并不是父级绝对定位的定位基准是最近的position非static祖先这个祖先叫 offsetParent。如果你把子元素设成absolute父级忘了设position: relative它就会继续往上找找到一个有 position 的祖先甚至直接以视口为基准。我之前做一个弹窗遮罩容器结构是.overlay .mask .dialog.overlay设了position: relative.dialog用absolute transform居中。本来没问题后来有人把.overlay的position改成了static结果整个弹窗立刻飞到页面左上角区域因为它的 offsetParent 变成了 body。排查技巧在 DevTools 里点中绝对定位元素计算标签页会直接标注出包含块。看到定位基准和预期不符往上一层层找那个非 static 的祖先即可。另外注意transform、filter、will-change这些属性也会创建包含块即使元素没有设置 position。这个行为很隐蔽遇到fadeIn 动画一触发弹窗位置就变了这种诡异现象先检查是不是某个祖先的transform改成了非none。4.3 transform 被覆盖动画和居中打架这是方式 A 最常见的失效场景。我做一个悬浮提示框定位用.tooltip { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }后来为了加一个浮出动效.tooltip { animation: pop 0.2s ease; } keyframes pop { from { transform: scale(0.5); } to { transform: scale(1); } }动画结束后transform的最终值是scale(1)居中的translate(-50%, -50%)被丢掉提示框整体往右下方偏移半个身位。这种问题用 DevTools 看 Computed 里的 transform 才能直观看到值被覆盖。解决思路有三个一是用margin负值或margin: auto方案替代 transform二是在动画里带上translate比如from { transform: scale(0.5) translate(-50%, -50%); }这样写但要注意代码冗余且容易写错顺序三是结构上包一层内层 div外层负责绝对定位居中内层负责动画。我更推荐第三种职责分离最清晰。4.4 子元素尺寸失控时stretch 会帮倒忙flex 容器默认align-items: stretch当子元素没有明确高度时它会被拉长对齐到容器高度。如果这个子元素里又有内容自身撑出的高度就会出现子元素明明写的居中视觉上却边缘贴边的情况。反过来当子元素比容器还大时align-items: center会让它上下均匀溢出但溢出的部分看不到顶和底因为溢出的方向同时也是滚动的方向内容被遮挡了。这个问题在做图片预览时特别明显一张超高的大图放在display: flex; align-items: center里图片顶部超出容器你往上滚也滚不动。解决办法是大尺寸内容用overflow: auto包一层让滚动发生在子区域而不是容器本身或者用align-items: flex-startmargin: auto组合让子元素在可滚动区域里居中同时保留溢出后的正常滚动行为。后者是我在做图片裁切预览时验证过的招比单纯居中可靠得多。5. 做项目时的选型决策直接照抄的判断逻辑最后整理一份我在真实项目里用的决策路径不算多标准但至少让我少踩了很多次坑。场景首选方案理由现代浏览器、内容尺寸未知绝大多数业务场景flex 或 grid代码量最少原理直观子元素不需要配合弹窗/浮层需要兼容老浏览器absolute margin 负值或 margin: auto不依赖 flex 兼容性定位稳定弹窗带复杂进出场动画absolute margin: auto 动画做在内层避免 transform 值被动画覆盖职责清晰单行按钮/标签文字居中line-height 等于容器高度开销最小渲染最快代码最简单多行文本块居中flex 或 table-cell不受换行影响高度自适应大图预览/内容可能超过容器overflow: auto flex-start margin: auto保留滚动能力不被裁剪需要同时居中且限定最大宽高grid place-itemsmax-widthgrid 对子元素尺寸约束更友好不易撑破里面的通用原则就三条移动端不要死磕 100vh 做参考系动画元素的 transform 和定位逻辑必须分离子元素尺寸不确定时优先容器侧解决方案父级控制而不是子元素侧transform 自我偏移。实际开发中我搞砸的次数比成功的多记下来也无非是给自己提个醒。曾经在项目里同时遇到三种坑——弹窗层级里有 transform 创建了新的包含块、fixed 定位因为祖先 transform 变回 absolute 行为、loading 动画又本机测试正常但上线后因为字体加载撑高了按钮导致居中偏移——那一次让我彻底学会了一件事居中代码写得再漂亮没有一个稳定的布局上下文该歪照样歪。把布局上下文先驯服再考虑居中方案顺序不能反。
返回列表