接触过一阵子 CSS 的人,迟早会被一个问题拦住:怎么把一个元素稳稳地放到页面正中间?我最早学前端的时候,水平居中用 margin: 0 auto,垂直居中要么算行高,要么用绝对定位加 transform,稍不注意整块布局就崩掉。后来 Flex 弹性布局出现,一行 display: flex 配上两个对齐属性,随手一写就是自适应居中。这一讲把 Flex 从头到尾捋一遍,零基础也能跟着试,看完之后你不但能应付日常布局,还能顺手解决不少前端面试题里爱考的 flex 细节。
1. 为什么我劝你直接啃 Flex:一套模型吃掉九成布局需求
1.1 传统布局的“居中困难症”到底难在哪
在 Flex 出现之前,前端的居中就是一门玄学。
- 块级元素水平居中:需要一个固定宽度,然后
margin: 0 auto。没有宽度?失效。 - 文本水平居中:
text-align: center,只对行内内容有效,块级元素内部依旧我行我素。 - 垂直居中:单行文本可以用
line-height模拟,多行文本就要用table-cell的vertical-align魔改。 - 最简单粗暴的是
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%)。这个方案确实好用,但绝对定位会脱离文档流,用它去排一整个页面,后面维护起来就是灾难。
这些方法不是不能用,而是“一个场景一个方案”,没有统一的心智模型。Flex 把问题抽象成了两个角色:容器和项目。所有对齐、伸缩、换行都围绕这两个角色展开,思考方式一下就统一了。
1.2 Flex 的适用边界:能做什么,别拿来做什么
先给没基础的同学划一条线:Flex 是一维布局模型。它擅长解决“一排元素怎么排”“一个元素怎么在区域内对齐”这类问题,典型场景包括导航栏、按钮组、卡片列表、表单元件、弹窗居中等。我这些年做项目,日常布局里八成以上都能用 Flex 解决。
那 Grid 呢?Grid 是二维布局,适合做“既有行又有列”的整体页面骨架,比如整个控制台的仪表盘、商品列表的栅格。Flex 和 Grid 不是二选一的关系,而是互补关系。新手我建议先啃 Flex,因为它的推理链短,属性数量少,能快速建立空间感;Grid 可以在 Flex 熟练之后再上手。
还有一个纪律性问题:别把一个页面所有区域都套上 Flex。遇到复杂页面就层层嵌套 flex,会导致父容器和子容器之间的伸缩规则互相干扰,调试成本极高。判断标准很简单:这段内容是不是只沿着一条轴排?如果不是,考虑 Grid 或者重新拆分模块。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搞清楚“父容器”和“子项目”:两组属性别混着记
2.1 主轴与交叉轴:所有对齐规则都从这里推导
Flex 有两个轴:主轴和交叉轴。默认情况下,主轴方向是水平向右,交叉轴方向是垂直向下。所有属性名的记忆都建立在“轴”的概念上。
css复制.container {
display: flex;
flex-direction: row; /* 默认值,主轴向右 */
}
属性 flex-direction 决定主轴方向:
row:主轴向右,项目从左往右排列。row-reverse:主轴向左,项目从右往左排列,视觉顺序会反转。column:主轴向下,项目从上往下排列。column-reverse:主轴向上,项目从下往上排列,视觉顺序反转。
排布方向一变,justify-content(主轴对齐)和 align-items(交叉轴对齐)的作用也跟着变。比如导航栏是水平排,主轴对齐就是横向对齐;侧边栏如果是纵向排,主轴对齐就变成纵向对齐。
我自己记法很简单:先写 flex-direction,把主轴在脑子里画出来,再谈对齐。
2.2 容器上的六个核心属性逐个说
这几个属性都写在父容器上,直接作用于所有直接子元素。
display
css复制.container {
display: flex;
}
把当前元素变成块级 Flex 容器,直接子元素变成 flex item。还有一个 display: inline-flex,效果是容器本身变成行内块级表现,适合做按钮内的小型布局,但日常用得少。
flex-direction
控制主轴方向,刚才已经说了。重点说一下 row-reverse 和 column-reverse,很多人在这里翻车:它不只是“换个方向”,它会把项目的起始位置反转。比如原本从左到右排列的 1、2、3,row-reverse 之后是 3、2、1,起始边变成了右侧。
flex-wrap
默认值是 nowrap,意思是不换行。很多新手困惑“为什么我设置了宽度 200px,两个项目却挤到只有 150px”,就是因为父容器宽度不够,而 nowrap 下项目默认会收缩。想把项目排不下时自动换行,就设 flex-wrap: wrap。这个属性和 flex-shrink 经常互相影响,后面第 5 节详细讲。
justify-content
控制主轴方向的对齐方式:
flex-start:向主轴起始边对齐。flex-end:向主轴结束边对齐。center:主轴居中。space-between:首尾贴边,中间平均分配空间。space-around:每个项目两侧有相等的空间,注意首尾的间距是中间的一半。space-evenly:所有间隙完全相等,首尾也有间距。
这里的“空间”是容器剩余空间。如果项目本身已经占满容器宽度,justify-content 就没有可分配空间,看起来不会动。
align-items
控制交叉轴方向的单行对齐方式:
stretch:默认值。如果子项目没有设置交叉轴方向的尺寸(比如横向排时没有设高度),会被拉伸填满整个容器高度。flex-start:交叉轴起始边对齐。flex-end:交叉轴结束边对齐。center:交叉轴居中。baseline:按第一行文字的基线对齐,多行文本场景比较好用。
这里最容易踩的坑是 stretch。你写了一个 display: flex 的容器,里面没设高度的按钮突然长得和旁边一样高,大概率就是 stretch 在起作用。
align-content
这个属性和 align-items 非常像,但只作用于“多行项目”的整体分配。当 flex-wrap: wrap 且内容确实折成多行时,align-content 控制这几行在交叉轴方向怎么分布。它同样支持 flex-start、flex-end、center、space-between、space-around、stretch 这些值。
如果容器只有一行,align-content 不生效。这是面试题里非常高频的一个点:align-content 为什么没反应?先看是不是 flex-wrap: nowrap,再看是不是只有一行。
2.3 最容易混淆的 justify-content 与 align-items
一张表记住两个属性的分工:
| 属性 | 控制方向 | 默认值 | 记忆锚点 |
|---|---|---|---|
justify-content |
主轴方向 | flex-start |
主轴,main axis |
align-items |
交叉轴方向 | stretch |
交叉轴,cross axis |
align-content |
多行整体在交叉轴方向 | stretch |
只在多行时生效 |
默认 flex-direction: row 时,justify-content 管左右,align-items 管上下。一旦改成 flex-direction: column,两者管的方向就交换。这也是面试官最爱问的“为什么 justify-content: center 没有让元素垂直居中”——因为主轴方向是水平,它只能管水平居中。
容器上还有一个实用属性 gap,用来设置项目与项目之间的间距:
css复制.container {
display: flex;
gap: 16px;
}
它和 margin 的效果类似,但不会像 margin 那样把间距算进项目自身的布局边界,代码更干净。老浏览器需要退到 margin 方案,新项目直接用 gap 就好。
3. 子项目上的属性:flex-grow、flex-shrink、flex-basis 的加减法
3.1 flex-basis 是“期望尺寸”,不是最小宽度
容器属性决定了整体的排布,真正决定“每个项目长多宽”的,是子项目自己的属性。flex-basis 先出场,它的作用是声明项目在主轴方向上的期望基础尺寸。
css复制.item {
flex-basis: 200px;
width: 300px; /* 当主轴为水平方向时,flex-basis 优先级更高 */
}
当 flex-basis 不是 auto 时,它在主轴方向上的权重高于 width。如果项目处于 column 方向,主轴是垂直方向,那么优先级压过的是 height。这个细节很多人记不住,实践时一改 flex-direction 就乱了。
flex-basis: auto 表示不额外指定基础尺寸,直接使用 width 或 height 的值;如果项目本身没有尺寸,就按内容大小来。
3.2 放大和缩小到底怎么计算
很多人只知道 flex-grow 是放大,flex-shrink 是缩小,真正问到底怎么算就答不上来。下面用一个具体例子推一遍。
放大场景
假设容器宽度 900px,三个项目的基础尺寸都是 200px,合计 600px,剩余空间 300px。设置项目的 flex-grow 分别为 1、1、2。
分配逻辑:剩余空间按 flex-grow 比例分。
- 项目1:300 × (1/4) = 75px,最终宽度 275px
- 项目2:300 × (1/4) = 75px,最终宽度 275px
- 项目3:300 × (2/4) = 150px,最终宽度 350px
缩小场景
如果容器宽度只有 600px,而三个项目的基础尺寸是 200px、200px、300px,合计 700px,溢出 100px。设置项目的 flex-shrink 分别为 1、1、2。
这里不能直接按 1:1:2 分,因为还要考虑每个项目的基础尺寸大。
- 权重 = flex-shrink × flex-basis
- 项目1权重 = 1 × 200 = 200
- 项目2权重 = 1 × 200 = 200
- 项目3权重 = 2 × 300 = 600
- 总权重 = 1000
每个项目需要减少的宽度 = 溢出总量 × (自身权重 / 总权重)。
- 项目1:100 × (200 / 1000) = 20px,最终 180px
- 项目2:100 × (200 / 1000) = 20px,最终 180px
- 项目3:100 × (600 / 1000) = 60px,最终 240px
所以 flex-shrink 不是一个绝对的“快慢”,而是结合基础尺寸一起算的“总伸缩权重”。这就是为什么两个 flex-shrink: 1 的项目,宽度不一定是等比缩小。
3.3 flex 简写的三种常见写法以及陷阱
手写三个属性太啰嗦,实际开发基本都用 flex 简写。三种最常见写法:
flex: 1等价于flex: 1 1 0%,表示所有项目从基础尺寸 0 开始,按剩余空间等比放大。最常用于等分空间。flex: auto等价于flex: 1 1 auto,表示基础尺寸按内容或 width 来,剩余空间再分配。适合“希望内容顶着内容走,但有多余空间时自动扩展”的场景。flex: none等价于flex: 0 0 auto,完全按照内容或 width,不放大也不缩小。
面试里经常问 flex: 1 是什么意思。flex: 1 是非常强制的“均分策略”,因为 flex-basis: 0% 抹掉了内容宽度的影响,所有项目都从零开始按 grow 比例分配容器宽度。如果你想保留内容宽度再分配剩余空间,应该用 flex: auto。
3.4 align-self、order:局部调整与视觉重排
align-self
align-self 写在单个子项目上,用来覆盖容器的 align-items 设置。比如容器统一 align-items: center,但某个项目偏偏想靠上,直接给它单独设一个 align-self: flex-start 即可。注意它覆盖的只是交叉轴方向,管不到主轴。
css复制.item-special {
align-self: flex-end;
}
order
order 控制项目在视觉上的排列顺序,默认值为 0。order: 1 会排到所有默认序号的后面,order: -1 会排到最前面。这个属性适合在某些场景下做视觉重排,比如移动端把图片放到文字后面。
我强烈建议不要用 order 做核心交互顺序调整,因为它只改视觉顺序,DOM 顺序和键盘焦点顺序不会变。如果你把按钮用 order 移到前面,但键盘 Tab 仍然按原 DOM 走,可访问性就崩了。真要改顺序,请改 HTML 结构,或者考虑用 CSS Grid 的区域定位。
4. 五类“自适应居中”场景:直接抄作业,再拆解为什么这样写
4.1 场景一:不管宽高多少,水平垂直居中
这个场景是绝对高频,弹窗、登录框、空状态提示全会遇到。核心就两行:
html复制<div class="overlay">
<div class="card">内容</div>
</div>
css复制.overlay {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.card {
width: 300px;
height: 200px;
background: #fff;
border-radius: 8px;
}
align-items: center 负责交叉轴居中,justify-content: center 负责主轴居中。关键是这种方式不要求 .card 有固定宽高,即使内容宽度不定,也能保持在中心位置。相比绝对定位加 transform,它不需要知道自身尺寸,也不会脱离文档流,更适合整体页面布局。
如果你希望 .card 高度撑满容器,而不是居中,就把 align-items: center 去掉,让 stretch 生效。这个“去掉即拉满”的效果也是 Flex 很好用的地方。
4.2 场景二:导航栏左中右三段,中间真正自适应
页面顶部导航栏常见结构:左边 Logo,中间标题或搜索框,右边用户操作。最直接的写法是:
html复制<header class="nav">
<div class="logo">Logo</div>
<div class="center">标题</div>
<div class="user">用户</div>
</header>
css复制.nav {
display: flex;
align-items: center;
height: 60px;
border-bottom: 1px solid #eee;
}
.center {
flex: 1;
text-align: center;
}
flex: 1 让中间区域吃掉所有剩余宽度,左边 Logo 和右边用户区域保持自身宽度,中间区域自然自适应。这里的“居中”是相对剩余区域居中,不是整屏居中。如果左右两侧宽度不一样,中间文字并不会精确落在屏幕正中心。
想做到字面意义上的整屏居中,就改成左右两侧宽度一致,或者让中间元素用绝对定位加 left: 50%; transform: translateX(-50%)。这是很多布局设计里容易忽略的细节,做视觉稿还原时要特别留心。
4.3 场景三:多行卡片两端对齐,最后一行不满怎么办
卡片列表经常要“一行排四个,两端贴边”。用 Flex 最直接:
css复制.list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.item {
flex: 0 0 calc(25% - 12px);
height: 120px;
background: #f0f0f0;
}
flex: 0 0 calc(25% - 12px) 表示不放大、不缩小,基础宽度固定为四分之一减去间距补偿。这里 12px 的来源是每行有 4 个卡片,3 个间隔,总共 3 × 16px = 48px,摊到每个卡片上就是 48 / 4 = 12px。
问题来了:如果最后一行只有 2 个卡片,它们会一左一右贴着两端,中间空一大块。想让最后一行的卡片保持左对齐,可以给列表加一个“幽灵占位”:
css复制.list::after {
content: "";
flex: 0 0 calc(25% - 12px);
}
但如果项目本身也是动态数量,::after 只能解决最后一个补位。更稳妥的方案是这种固定列数的场景直接改用 CSS Grid:
css复制.list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
Grid 对“列数固定”的控制比 Flex 干净太多。所以,Flex 适合数量不固定、内容流式换行的场景;只要是列数固定,我建议你认真考虑 Grid。这也是很多工程师从 Flex 进阶到 Grid 的真实路径。
4.4 场景四:等分布局与响应式换行
有时候你不知道卡片到底有几个,但希望它们在可用宽度内自动均分,放不下就换行。flex: 1 1 200px 是一个很实用的组合:
css复制.cards {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.card {
flex: 1 1 200px;
min-width: 0;
background: #f8f8f8;
</kbd>
padding: 24px;
}
拆解一下:
flex-grow: 1:有剩余空间就等分扩展。flex-shrink: 1:宽度不够可以压缩。flex-basis: 200px:理想基础宽度是 200px,作为换行阈值。
当容器宽度在 400px 到 600px 之间时,卡片会按两列排列;更宽时自动变成三列、四列。加 min-width: 0 是为了防止内容过宽把卡片撑破,这个坑我们下一节单独说。
等分场景还有一个更直接的写法:flex: 1 1 0%,所有卡片从 0 开始成长,最终宽度完全相等。但要注意,如果卡片内部有很长的文本,没有 min-width: 0 的情况下会导致内容溢出而不是压缩。
4.5 场景五:经典三栏圣杯布局的 Flex 简化版
圣杯布局在传统布局时代要写一堆 hack,用 Flex 可以压到很少的代码:
html复制<body>
<header>头部</header>
<div class="container">
<aside class="left">左侧栏</aside>
<main class="main">主内容</main>
<aside class="right">右侧栏</aside>
</div>
<footer>底部</footer>
</body>
css复制body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
header,
footer {
flex-shrink: 0;
padding: 20px;
background: #f0f0f0;
}
.container {
display: flex;
flex: 1;
min-height: 0;
}
.left,
.right {
flex-shrink: 0;
width: 200px;
background: #eaeaea;
}
.main {
flex: 1;
min-width: 0;
background: #fff;
}
@media (max-width: 600px) {
.container {
flex-direction: column;
}
.left,
.right {
width: auto;
}
}
思路拆解:
body设成纵向 Flex 容器,min-height: 100vh让页面至少撑满一屏。.container用flex: 1吃掉中间剩余高度,保证 footer 能被推到最底部。- 左右栏固定宽度禁止收缩,主内容
flex: 1自适应。 - 移动端用媒体查询把
.container改成纵向,三栏就自然堆叠。
这里有几个隐藏点:.container 如果没有 min-height: 0,在某些浏览器里可能被内部内容撑大,导致 footer 被推下去。min-width: 0 同样是为了防止长文本把主内容撑破。这类“0 界限”是 Flex 老手和新手之间的一道分水岭。
5. 那些年 Flex 翻过的车:我把踩过的坑列给你
5.1 子内容把项目撑破:min-width: 0 与 white-space
这是个经典翻车现场。你写了一个 Flex 布局,左边是头像,右边是一段很长很长的文本,文本设置了 white-space: nowrap,结果整个容器被撑出横向滚动条。
原因是 flex item 默认 min-width: auto,也就是说它的最小宽度是“内容的最小宽度”。flex-shrink 虽然能让项目在宽度不够时收缩,但遇到 white-space: nowrap 或者很长的 URL、英文单词时,内容宽度成了硬下限,项目缩不下去。
解决办法是给这个项目设置 min-width: 0,允许它的宽度被压缩到比内容宽度更小:
css复制.text-block {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
如果主轴方向是纵向,对应的问题就是 min-height: 0。只要看到 flex 项目内容溢出、宽高不随容器变化,先怀疑是不是内容把最小尺寸顶住了。
5.2 margin: auto 在 Flex 中的隐藏玩法
Flex 里 margin: auto 会比普通布局更“好用”,因为 auto 会自动吸收主轴或交叉轴上的剩余空间。
想象你有一个按钮组,想让“登录”按钮贴到最右侧,其他按钮靠左。你不需要写 justify-content: space-between,直接给想要推到右边的按钮设置:
css复制.group {
display: flex;
}
.login {
margin-left: auto;
}
更进一步的用法是 margin: auto 同时吸收多个方向的剩余空间,实现完美居中:
css复制.box {
display: flex;
width: 300px;
height: 200px;
}
.center {
margin: auto;
}
这段代码不需要 justify-content 和 align-items,margin: auto 会把所有剩余空间吃掉,把项目推到正中心。这在某些动态场景比写死对齐属性更灵活,比如一行里有多个元素,其中一个需要自动靠右、另一个固定居中,用 margin-left: auto 做局部推进比容器属性更容易控制。
5.3 子元素宽度 100% 失效是怎么回事
有人说“我给 flex 子元素设了 width: 100%,结果它并没有占满一行,甚至被挤得很窄”。原因通常是 flex-shrink 默认是 1,当父容器空间不足时,这个 100% 的“期望尺寸”会被压缩。
css复制.parent {
display: flex;
}
.child {
width: 100%;
/* flex-shrink 默认 1,空间不够时会被压缩 */
}
想让子元素稳定占满一整行,直接用:
css复制.child {
flex: 0 0 100%;
}
flex: 0 0 100% 表示不放大、不缩小、基础宽度 100%,项目就不会被压缩。如果你想保留一个固定宽度但允许按比例缩放,就不要用 100%,而是用 flex: 1 1 auto 或者 width 配合 flex-shrink: 0。
还有一种“失效”是 flex-basis 覆盖了 width,前面说过,flex-basis 不是 auto 时,主轴尺寸以它为准。这两个原因容易混淆,排查时先看 flex-basis,再看 flex-shrink。
5.4 面试高频:flex: 1 是什么、align-content 为什么不生效
前端面试题里 Flex 是常客,高频问题基本就这几个:
flex: 1等价于什么?答:flex: 1 1 0%,项目从基础尺寸 0 开始,按 grow 比例分配空间。flex: auto和flex: 1的区别?答:flex: auto是1 1 auto,会先按内容或 width 计算基础尺寸,再分配剩余空间。align-items和align-content的区别?答:前者管单行项目在交叉轴上的对齐,后者管多行整体在交叉轴上的分布。align-content不生效,先检查flex-wrap是不是nowrap,再检查是否只有一行。justify-content: center为什么不能让元素垂直居中?答:因为justify-content作用于主轴,默认主轴水平,它只能做水平居中;垂直居中要改flex-direction: column或使用align-items: center。
如果你的项目还需要兼容特别老的 WebKit 内核,必要时加 display: -webkit-flex 等前缀。不过现在新项目基本不需要,这个知识更多是解决历史包袱用。
最后分享一个我自己的习惯。我写 flex 布局时永远是先定容器,再调项目:先写 display: flex,再写 flex-direction,接着想清楚主轴是横还是纵,然后写 justify-content 和 align-items,最后一个一个处理子项。顺序一固定,很多属性混淆问题自然就消失了。你要是刚接触,也建议用这个顺序练上一两个星期,肌肉记忆形成之后,Flex 就是你写页面时最顺手的那把锤子。
