Flexbox布局完全指南

张玥 2026年9月20日 阅读时间 45分钟
Flexbox CSS布局 响应式 一维布局 最佳实践
前端开发技巧之Flexbox布局完全指南

如果你只能从 CSS 里保留一个布局模块,绝大多数前端工程师都会选 Flexbox。它解决了困扰 Web 布局十几年的三大难题:垂直居中、等高列、剩余空间分配。在它出现之前,我们靠 float 加清除浮动、靠 inline-block 加负边距、靠 table-cell 模拟垂直居中,每一种方案都像在跟浏览器斗智斗勇。Flexbox 把这些“技巧”变成了声明式的属性——你只需要说明“我希望这些盒子怎么排列”,剩下的计算交给浏览器。但真正把它用好的项目并不多:有人把 flex: 1 当成万能药,结果遇到 min-width: auto 的溢出谜题;有人用 order 调整视觉顺序,却破坏了键盘 Tab 的浏览次序;有人在只需要一维排列的地方硬套 Grid,又在需要二维对齐的地方死磕 Flex。这篇 45 分钟的长文,会从心智模型讲起,逐一拆解全部容器属性与项目属性,用可交互的实验室让你亲眼看到每个取值的效果,再深入到尺寸计算公式、典型布局模式、响应式策略、无障碍陷阱、调试方法,最后给出一份可以贴在工位上的检查清单。读完它,你应该能在任何布局需求面前,第一秒就判断出该用哪个属性。

一、从浮动到弹性盒:为什么它改变了一切

要理解 Flexbox 的价值,得先回忆一下它之前的世界。2010 年前后,一个“两栏布局、左栏固定宽度、右栏自适应、两栏等高”的需求,标准答案是这样的:

/* 浮动时代的经典两栏布局 */
.layout { overflow: hidden; /* 清除浮动 */ }
.left {
  float: left;
  width: 240px;
  padding-bottom: 9999px;
  margin-bottom: -9999px; /* 伪等高 hack */
}
.right {
  margin-left: 240px;
  padding-bottom: 9999px;
  margin-bottom: -9999px;
}

这段代码里有三个“魔数”:9999px 的 padding-bottom 配 -9999px 的 margin-bottom,靠父容器的 overflow: hidden 裁掉多余部分,制造出“两栏等高”的假象。它能让页面看起来对,但任何一个后来维护的人都得花十分钟才能想明白为什么。更别提它还会带来溢出裁剪、打印样式异常、无法做垂直居中等一系列副作用。

Flexbox 用一个属性解决了这一切:align-items: stretch——这本来就是它的默认值。两栏自动等高,不需要任何 hack。

Flexbox 的三个根本性突破

空间分配 剩余空间可以按比例分给子项,也可以分给子项之间的间隙,不需要计算百分比
对齐控制 主轴与交叉轴各有独立的对齐方式,垂直居中变成一行代码
顺序解耦 视觉顺序可以独立于 DOM 顺序(但请谨慎使用,见第十章)
尺寸协商 子项可以“放大”也可以“收缩”,由浏览器统一求解,而不是靠百分比猜

一维布局:Flexbox 的能力边界

理解 Flexbox 最重要的一点是:它是一维布局模型。它把子项排成一行(或者一列),然后在这个方向上分配空间、在垂直方向上对齐。它不擅长“同时控制行和列”的二维布局——那是 CSS Grid 的领域。

这个边界非常重要,因为它决定了你什么时候该用谁:

需求 推荐方案 原因
导航栏、工具栏、按钮组 flex 单行排列,需要两端对齐或均分
垂直水平居中 flex 两行属性搞定,兼容性最好
卡片列表自动换行 flex + wrap 一维流式排列,宽度自适应
整页网格布局 grid 需要同时控制行与列
表单的标签—控件对齐 grid 或 flex Grid 在多行标签对齐时更省事
不规则瀑布流 grid / columns Flex 无法跨行对齐
常见误区
“Flexbox 万能,什么布局都用它。”——用 Flexbox 硬做二维网格,只能靠嵌套多层容器,代码量和维护成本都会翻倍。
正确姿势
“一维用 Flex,二维用 Grid,两者嵌套组合。”页面级骨架用 Grid,组件内部用 Flex,是最常见的搭配。

二、核心心智模型:主轴与交叉轴

Flexbox 所有的属性,都可以归到两条轴上:主轴(main axis)和交叉轴(cross axis)。理解这一点,你就理解了 Flexbox 的 80%。

主轴起点 主轴终点 交叉轴起点 交叉轴终点
项目 1
项目 2
项目 3

默认状态下 flex-direction: row,主轴水平向右,交叉轴垂直向下。

四条规则,记住就不会乱

  • flex-direction 决定主轴方向。主轴一旦改变,交叉轴自动跟着旋转 90 度。
  • justify-content 永远作用在主轴上——控制子项沿主轴如何排布。
  • align-items 永远作用在交叉轴上——控制子项沿交叉轴如何对齐。
  • flex-grow / flex-shrink / flex-basis 也作用在主轴方向上的尺寸。

很多人第一次遇到 flex-direction: column 时会困惑:“为什么 justify-content: center 变成了垂直居中?”答案就是——主轴转了。此时主轴垂直向下,justify-content 自然作用于垂直方向,而 align-items 变成了水平方向。

亲手验证:主轴旋转实验

下面这个实验室把 flex-direction、justify-content、align-items、align-content、flex-wrap 和 gap 全部暴露出来。建议你依次尝试:

  1. 把 flex-direction 改成 column,观察 justify-content 的作用方向发生了什么变化。
  2. 把 justify-content 从 flex-start 依次改成 center、space-between、space-around、space-evenly,对比它们留白分配的差异。
  3. 把 flex-wrap 改成 wrap,然后调整 align-content,观察多行时行与行之间的分布。
  4. 拖动 gap 滑块,感受它和 margin 的区别——gap 只在项目之间产生间距,不会在边缘留白。
1
2
3
4
5

关于 align-content:最容易被误会的属性

初学者常常把 align-content 和 align-items 搞混。一句话区分:

  • align-items:控制单个项目在交叉轴上的对齐方式。
  • align-content:控制多条轴线在交叉轴上的分布方式——只有在 flex-wrap: wrap 且实际产生了多行时才生效。

如果你的容器是单行(nowrap),那么 align-content 完全没有任何效果。这解释了大量“我写了 align-content: center 但没反应”的疑问。

三、容器属性全解:把容器变成一个弹性上下文

所有 Flexbox 属性分成两类:写在容器上的(控制整体)和写在项目上的(控制个体)。先把容器属性吃透。

3.1 display:一切从这里开始

/* 块级弹性容器:占满可用宽度,可设宽高 */
.container { display: flex; }

/* 行内弹性容器:宽度由内容决定,像行内元素一样参与排版 */
.badge-group { display: inline-flex; }

一旦元素成为弹性容器,会发生三件重要的事:

  • 它的直接子元素全部变成弹性项目(flex item),不再遵守常规流的 display 表现。
  • 子元素的 float、clear、vertical-align 全部失效——它们不再属于常规流。
  • 子元素的 margin 不会合并(margin collapse),这既是好事也是坑。

3.2 flex-direction:定义主轴

点击下面每个选项,看看它把主轴指向哪里。

1
row
默认值
主轴水平向右,项目从左到右排列。在从左到右的书写模式下(如中文、英文),起点在左边。
2
row-reverse
水平反向
主轴水平向左,项目从右到左排列。起点变成右边,因此 justify-content: flex-start 会把项目推到右侧。
3
column
垂直主轴
主轴垂直向下,项目从上到下排列。justify-content 控制垂直分布,align-items 控制水平对齐。
4
column-reverse
垂直反向
主轴垂直向上,项目从下到上排列。常用于聊天记录、日志列表这类“新内容在下方但视觉从底部开始”的场景。

需要特别提醒的是:row 与 row-reverse 改变的不只是视觉顺序,还包括“起点”的定义。这会影响 justify-content: flex-start / flex-end 的语义,也会影响 order 的排列基准。

3.3 flex-wrap:让项目可以换行

默认情况下,Flexbox 会把所有项目硬塞进一行,宁可压缩项目也不换行。这就是经典的“为什么我的项目被压扁了”问题。flex-wrap: wrap 让容器允许换行:

.tags {
  display: flex;
  flex-wrap: wrap; /* 关键:允许换行 */
  gap: 8px;
}

三个取值的差异:

  • nowrap(默认):不换行,项目会被压缩(flex-shrink 生效),或者溢出容器。
  • wrap:换行,第一行在上方,后续行依次向下。
  • wrap-reverse:换行,但行序反转——第一行在下方。交叉轴的起点也跟着翻转。

flex-flow 是 flex-direction 和 flex-wrap 的简写,顺序不限:

/* 下面两行完全等价 */
.a { flex-flow: row wrap; }
.b { flex-direction: row; flex-wrap: wrap; }

3.4 justify-content:主轴上的分布

这是使用频率最高的属性。它的六个取值可以分成两类:

取值 效果 典型场景
flex-start 所有项目靠主轴起点 左对齐的默认布局
flex-end 所有项目靠主轴终点 右对齐的按钮组
center 项目整体居中,两端留白相等 水平居中
space-between 首尾贴边,中间等分 导航栏 logo 与菜单两端对齐
space-around 每个项目两侧留白相等,边缘留白是中间的一半 工具栏图标均匀分布
space-evenly 所有间隙(含边缘)完全相等 需要绝对均匀的分布

很多人分不清 space-around 和 space-evenly。用一句话记住:space-around 的边缘留白是项目之间间隙的一半,space-evenly 的边缘留白与项目之间间隙完全相同。

关于 safe / unsafe 对齐

在项目溢出时,居中对齐会导致首尾内容被裁掉且无法滚动到。CSS 提供了 safe 关键字来避免:

/* 空间足够时居中;空间不足时退化为 flex-start,保证内容可见 */
.toolbar { justify-content: safe center; }

它的浏览器支持还不算全面,但在横向滚动容器里非常值得加上,配合 overflow-x: auto 使用效果最好。

3.5 align-items:交叉轴上的对齐

  • stretch(默认):项目在交叉轴方向拉伸填满容器——前提是项目没有设置该方向的具体尺寸。这就是“自动等高”的实现原理。
  • flex-start:交叉轴起点对齐(默认情况下是顶部)。
  • flex-end:交叉轴终点对齐(底部)。
  • center:交叉轴居中。
  • baseline:按第一行文字的基线对齐。这个取值在混排不同字号时非常有用,例如价格标签和原价放在一起。
/* 价格展示:大字价格与划线原价按基线对齐 */
.price-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.price-now { font-size: 2rem; font-weight: 700; }
.price-old { font-size: 0.9rem; text-decoration: line-through; }

为什么 stretch 有时候“不生效”

项目在交叉轴方向设了具体尺寸(比如 height: 40px),或者设了 align-self 覆盖,stretch 就不会起作用。另外,如果容器的交叉轴尺寸是 auto(内容撑开),拉伸也没有空间可拉伸。

3.6 align-content:多行之间的分布

再次强调:只在 flex-wrap: wrap 且产生了多行时生效。它的取值和 justify-content 一一对应,只不过作用在交叉轴上:stretch(默认,行被拉伸填满)、flex-start、flex-end、center、space-between、space-around。

3.7 gap:告别 margin 负边距

在 gap 出现之前,让弹性项目之间产生间距只有两种办法:给每个项目加 margin(然后在首尾用负边距抵消),或者用 :not(:last-child) 选择器。前者容易出错,后者在换行时完全不适用。

/* 现代写法:一行搞定,换行时也不会在行尾多出间距 */
.grid-of-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 24px; /* 行间距 列间距 */
}

/* 也可以分开写 */
.a { row-gap: 16px; column-gap: 24px; }

/* 旧写法:需要处理首尾 */
.old { margin: -8px; }
.old > * { margin: 8px; }

gap 的另一个优势是它不会在容器边缘产生留白,也不参与项目的尺寸计算,语义上更接近“间隙”而非“外边距”。

3.8 place-items 与 place-content

这两组简写属性同样适用于弹性容器(虽然它们最初是为 Grid 设计的):

/* align-items 与 justify-items 的简写(justify-items 对 flex 无意义) */
.a { place-items: center; }

/* align-content 与 justify-content 的简写 */
.b { place-content: center center; }

四、项目属性全解:控制每一个盒子的行为

容器属性决定“整体怎么排”,项目属性决定“个体怎么长”。三个最核心的项目属性是 flex-grow、flex-shrink、flex-basis,它们共同回答了同一个问题:容器里多余或不足的空间,该怎么分?

4.1 flex-grow:怎么“变大”

默认值是 0,意思是“有剩余空间也不放大”。如果设成大于 0 的数值,项目就按照数值比例分配剩余空间。

/* 容器 900px,三个项目各 200px,剩余 300px */
.item-a { flex-grow: 1; } /* 分到 100px,最终 300px */
.item-b { flex-grow: 1; } /* 分到 100px,最终 300px */
.item-c { flex-grow: 1; } /* 分到 100px,最终 300px */

/* 如果 a 是 2,b 和 c 是 1,总权重 4 */
/* a 分到 150px(最终 350px),b 和 c 各分到 75px(最终 275px) */

关键理解:flex-grow 分配的是“剩余空间”,而不是“容器总宽度”。项目的初始尺寸由 flex-basis(或 width)决定,剩下的部分才按 flex-grow 分。

4.2 flex-shrink:怎么“变小”

默认值是 1,意思是“空间不足时,按比例收缩”。如果设成 0,项目就完全拒绝收缩,可能导致溢出。

一个常见的场景是:图片放在弹性容器里被压变形。解决办法就是给它 flex-shrink: 0:

.media {
  display: flex;
  gap: 12px;
}
.media__avatar {
  width: 56px;
  height: 56px;
  flex-shrink: 0; /* 防止头像被压扁 */
}
.media__body {
  min-width: 0; /* 允许文本区域收缩,见 4.6 节 */
}

4.3 flex-basis:收缩与放大的基准

flex-basis 定义了项目在分配剩余空间之前的初始主轴尺寸。它的取值可以是:

.a { flex-basis: auto; } /* 默认:看 width,没有则看内容宽度 */
.b { flex-basis: 0; } /* 初始尺寸为 0,全部尺寸靠 grow 分 */
.c { flex-basis: 200px; } /* 固定初始尺寸 */
.d { flex-basis: 30%; } /* 相对容器主轴尺寸的百分比 */
.e { flex-basis: content; } /* 按内容固有尺寸,忽略 width */

flex-basis 与 width 谁优先?

当 flex-basis 不是 auto 时,它覆盖 width(在 flex-direction: row 下)或 height(在 column 下)。也就是说:

  • flex-basis: auto + width: 200px → 初始尺寸 200px。
  • flex-basis: 300px + width: 200px → 初始尺寸 300px,width 被忽略。
  • flex-basis: 0 + width: 200px → 初始尺寸 0,width 被忽略。

但要注意:width 并没有完全失效,它仍然会作为 min-width / max-width 的参考,也影响内容的换行计算。所以在实践中,建议只用一个——要么用 width,要么用 flex-basis,不要两者混用。

4.4 flex 简写:三个属性一次搞定

flex 是 flex-grow、flex-shrink、flex-basis 的简写。它的语法有几个容易踩坑的默认值规则:

简写 展开后 含义
flex: initial 0 1 auto 默认值:不放大,可收缩,按内容尺寸
flex: 1 1 1 0% 忽略内容尺寸,等分剩余空间
flex: auto 1 1 auto 先按内容尺寸,再等分剩余空间
flex: none 0 0 auto 完全不参与伸缩,保持原始尺寸
flex: 0 0 1 0% 不放大,可收缩,初始尺寸 0
flex: 2 3 100px 2 3 100px 完整写法,最清晰
易错点
flex: 1 的 basis 是 0% 而不是 auto。这意味着内容宽度完全不参与初始分配,两个内容长短差异很大的项目也会被分到一样宽。
正确姿势
如果需要“先按内容、再分剩余”,用 flex: auto;如果需要“严格等宽”,用 flex: 1。

4.5 亲手验证:项目属性实验室

下面的实验室把红色虚线框中的项目作为实验对象,其他两个项目保持固定。拖动滑块观察它的变化。

A
目标
C

建议尝试的几个组合:

  • flex-grow: 5 + flex-basis: 0 → 目标项目独占剩余空间。
  • flex-basis: 240px + flex-shrink: 0 → 目标项目撑破容器,产生溢出。
  • order: -2 → 目标项目跑到最前面(但 DOM 顺序没变)。
  • flex-basis: 40% → 初始尺寸按容器宽度计算。

4.6 order:视觉顺序的重新排列

order 的默认值是 0,数值越小越靠前。它只改变视觉顺序,不改变 DOM 顺序。

这在响应式设计里偶尔有用——比如移动端把“购买按钮”提到最前面:

/* 移动端:详情页的购买栏提到顶部 */
@media (max-width: 768px) {
  .buy-bar { order: -1; }
}

但请务必阅读第十章的“无障碍”部分——order 是 Flexbox 里最容易造成可访问性问题的属性。

4.7 align-self:单个项目的例外

当容器设了 align-items,但某个项目想单独对齐时,用 align-self 覆盖。它的取值和 align-items 完全一致,外加 auto(表示继承容器的 align-items)。

/* 卡片底部按钮靠下,其余内容靠上 */
.card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.card__action {
  align-self: flex-end; /* 只有这个按钮靠右 */
  margin-top: auto; /* 顺带把它推到容器底部 */
}

注意上面例子里的 margin-top: auto——在弹性容器中,auto 外边距会吸收所有剩余空间。这是一个极其有用的技巧,可以实现“把某几个项目推到另一端”的效果,比 justify-content 更灵活。

/* 导航栏:左中右三段式,中间的 logo 不动,两侧自动撑开 */
.navbar {
  display: flex;
  align-items: center;
}
.navbar__left { margin-right: auto; }
.navbar__right { margin-left: auto; }

4.8 min-width: auto:Flexbox 最著名的陷阱

这是几乎所有前端工程师都踩过的坑,值得单独用一节来讲。

在常规流中,块级元素的 min-width 是 0。但在弹性项目中,min-width 的初始值是 auto,意思是“不能小于内容的固有尺寸”。

这带来的直接后果是:当一个弹性项目里含有长文本、长单词或不换行的 pre 时,它会拒绝收缩到内容宽度以下,从而撑破容器。

/* 问题代码:这段文本会溢出容器 */
.row { display: flex; }
.row__text {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* 省略号不生效!因为 min-width 默认是 auto */
}

/* 修复:显式声明 min-width: 0 */
.row__text {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

同样的坑也存在于交叉轴方向,对应的是 min-height: auto。一个实用的经验法则是:

水平溢出 给收缩项加 min-width: 0
垂直溢出 给收缩项加 min-height: 0
省心做法 在全局样式里写 :where(flex容器) > * { min-width: 0; }
替代方案 overflow: hidden 或 overflow: auto 同样能重置自动最小尺寸

五、尺寸计算:浏览器到底在算什么

绝大多数时候,你不需要手动计算 Flexbox 的尺寸——浏览器会算。但当你遇到“为什么这个项目比那个宽”“为什么收缩后比例不对”这类问题时,知道算法就非常重要了。

5.1 三步走的分配流程

  1. 确定初始尺寸:每个项目的主轴尺寸 = flex-basis(若为 auto 则取 width,若也没有则取内容尺寸)。
  2. 计算剩余空间:容器主轴尺寸 − 所有项目初始尺寸之和 − 所有 gap 之和。
  3. 分配剩余空间:
    • 如果剩余空间为正 → 按 flex-grow 的比例分配。
    • 如果剩余空间为负 → 按 flex-shrink × flex-basis 的加权比例收缩。

5.2 放大计算实例

容器宽度 900px,gap 为 0,三个项目的 flex 值分别是:

.a { flex: 2 1 100px; }
.b { flex: 1 1 100px; }
.c { flex: 1 1 200px; }

计算过程:

  • 初始尺寸之和:100 + 100 + 200 = 400px
  • 剩余空间:900 − 400 = 500px
  • grow 权重之和:2 + 1 + 1 = 4
  • a 分到:500 × 2/4 = 250px,最终宽度 350px
  • b 分到:500 × 1/4 = 125px,最终宽度 225px
  • c 分到:500 × 1/4 = 125px,最终宽度 325px
  • 验证:350 + 225 + 325 = 900px ✓

5.3 收缩计算实例(重点)

收缩比放大复杂,因为它的权重不是 flex-shrink 本身,而是 flex-shrink × flex-basis。

容器宽度 600px,三个项目都没有设 flex-shrink(默认都是 1):

.a { flex: 0 1 300px; }
.b { flex: 0 1 200px; }
.c { flex: 0 1 200px; }

计算过程:

  • 初始尺寸之和:300 + 200 + 200 = 700px
  • 溢出量:700 − 600 = 100px(需要收缩 100px)
  • 加权收缩因子:300×1 = 300,200×1 = 200,200×1 = 200,总和 700
  • a 需要收缩:100 × 300/700 ≈ 42.86px,最终 257.14px
  • b 需要收缩:100 × 200/700 ≈ 28.57px,最终 171.43px
  • c 需要收缩:100 × 200/700 ≈ 28.57px,最终 171.43px
  • 验证:257.14 + 171.43 + 171.43 ≈ 600px ✓

这个例子告诉我们一个反直觉的事实:宽度大的项目收缩得更多。如果你希望所有项目“按比例均匀缩小”,需要设置 flex-basis: 0,让初始尺寸一致。

5.4 当项目拒绝收缩时

如果某个项目的 flex-shrink: 0,那么它完全不参与收缩计算,全部溢出量由其他项目按比例承担。如果其他项目的收缩能力也不足以吸收溢出量,容器就会产生溢出。

更隐蔽的是 min-width: auto 的介入:即使 flex-shrink 是 1,项目也不会缩到内容固有宽度以下。这就是“计算公式算出来的宽度和实际渲染宽度不一致”的根本原因。

5.5 一图流总结

场景 关键属性 推荐写法
严格等宽分栏 flex: 1 忽略内容,等分容器
按内容比例分栏 flex: auto 内容多的占得多,再分剩余
固定宽 + 自适应 flex: none + flex: 1 侧栏固定,主区自适应
可换行卡片 flex: 1 1 240px 基准宽度 + 允许放大
禁止压缩的图标 flex: none 保持原始尺寸
需要文本省略号 flex: 1 + min-width: 0 允许收缩到 0

六、典型布局模式实战

理论讲完,来看真实项目里最高频的十种布局。每一种都是可以直接复制粘贴的成熟方案。

6.1 完美居中:三行代码解决十年难题

.center-box {
  display: flex;
  justify-content: center; /* 主轴居中 */
  align-items: center; /* 交叉轴居中 */
  min-height: 100vh;
}

/* 简写版本 */
.center-box {
  display: flex;
  place-content: center;
  min-height: 100vh;
}

注意用 min-height 而不是 height:这样内容超出时容器还能长高,不会把内容裁掉。

6.2 导航栏:两端对齐的标准方案

<header class="navbar">
  <a class="navbar__logo">品牌</a>
  <nav class="navbar__nav">…</nav>
  <div class="navbar__actions">…</div>
</header>

.navbar {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 0 24px;
  height: 64px;
}
.navbar__nav {
  margin-left: auto; /* 推到右侧 */
}

6.3 两栏布局:固定侧栏 + 自适应主区

.layout {
  display: flex;
  gap: 32px;
  align-items: flex-start; /* 侧栏不被拉伸 */
}
.layout__aside {
  flex: 0 0 280px; /* 不放大不收缩,固定 280px */
}
.layout__main {
  flex: 1;
  min-width: 0; /* 关键:允许主区收缩 */
}

/* 移动端:改为上下堆叠 */
@media (max-width: 768px) {
  .layout { flex-direction: column; }
  .layout__aside { flex: none; width: 100%; }
}

6.4 卡片网格:自动换行的等宽卡片

这是响应式卡片列表最简洁的实现——不需要媒体查询,卡片会自动根据可用宽度决定每行放几个:

.card-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}
.card {
  flex: 1 1 240px; /* 基准 240px,可放大可收缩 */
  min-width: 0;
  max-width: 100%;
}

拖动下面的滑块,亲眼看看卡片如何自动重排。这就是 flex: 1 1 240px 加上 flex-wrap: wrap 的威力。

卡片 1
卡片 2
卡片 3
卡片 4
卡片 5
卡片 6
.card-grid { display: flex; flex-wrap: wrap; gap: 20px; } .card { flex: 1 1 240px; min-width: 0; }

用这种写法需要注意一点:当最后一行只有一两个卡片时,它们会被 flex-grow: 1 拉伸变宽,与其他行的卡片宽度不一致。如果需要严格等宽,把 flex-grow 改成 0:flex: 0 1 240px,这样最后一行不会被拉伸,但左侧会留白。

6.5 媒体对象:图文混排的经典结构

<div class="media">
  <img class="media__figure" src="avatar.png" alt="">
  <div class="media__body">
    <h3>标题</h3>
    <p>这是一段可能很长的描述文本,需要在空间不足时优雅地收缩。</p>
  </div>
</div>

.media {
  display: flex;
  align-items: flex-start; /* 顶部对齐,而非拉伸 */
  gap: 12px;
}
.media__figure {
  flex: none; /* 绝不压缩图片 */
  width: 56px;
  height: 56px;
  border-radius: 50%;
}
.media__body {
  flex: 1;
  min-width: 0; /* 允许文本区域收缩 */
}

6.6 页脚吸底:内容不足一屏也贴在底部

/* 方案一:整页弹性列布局 */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
main {
  flex: 1; /* 吸收所有剩余高度 */
}

/* 方案二:只给页脚加 margin-top: auto */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
footer {
  margin-top: auto; /* 自动吸底 */
}

方案二更简洁,也不需要给 main 加额外属性。注意 min-height 用在 body 上时,最好同时给 html 设 height: 100%,避免某些浏览器下的百分比高度问题。

6.7 输入框 + 按钮:让输入框吃掉剩余宽度

.search-bar {
  display: flex;
  gap: 8px;
}
.search-bar__input {
  flex: 1;
  min-width: 0;
}
.search-bar__btn {
  flex: none; /* 按钮保持原始宽度 */
}

注意:input 元素有自己的固有尺寸(默认约 20 个字符宽),所以必须加 min-width: 0 才能让它真正收缩。

6.8 标签列表:自动换行 + 均匀间距

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.tag-list > * {
  flex: none; /* 标签不拉伸也不压缩 */
  white-space: nowrap;
}

6.9 表格行:列对齐与省略号

.table-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 0;
}
.table-row > .col-name { flex: 0 0 160px; }
.table-row > .col-desc {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.table-row > .col-action { flex: none; }

6.10 分页条:居中 + 两端固定

.pagination {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-content: center;
}
.pagination__total { margin-right: auto; } /* 靠左 */
.pagination__jump { margin-left: auto; } /* 靠右 */

这个模式利用了弹性容器中 auto 外边距吸收剩余空间的特性,实现“三段式”布局,比嵌套三层 div 加 justify-content: space-between 更灵活。

七、响应式策略:让布局自己适配

Flexbox 天生适合响应式,因为它的很多属性都是“内容驱动”的——不需要精确知道容器宽度,浏览器会自动调整。这里有三种层次不同的响应式思路。

7.1 第一层:换行 + 弹性基准(零媒体查询)

这是成本最低的响应式方案。核心是给项目一个“最小可接受宽度”作为 flex-basis,容器放不下就自动换行:

.auto-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.auto-grid > * {
  flex: 1 1 280px;
  min-width: 0;
}

这段代码在宽屏下是 3~4 列,平板下是 2 列,手机上自动变成 1 列——全程不需要任何媒体查询。

7.2 第二层:媒体查询改变方向

当布局的结构需要改变(而不只是列数)时,媒体查询仍然是必要的:

.split {
  display: flex;
  gap: 32px;
  align-items: center;
}
.split__media { flex: 1 1 50%; }
.split__text { flex: 1 1 50%; }

@media (max-width: 720px) {
  .split {
    flex-direction: column;
    gap: 20px;
  }
  .split__media,
  .split__text { flex: none; width: 100%; }
}

7.3 第三层:容器查询

媒体查询看的是视口宽度,但很多时候组件关心的是自己所在的容器有多宽。比如同一个卡片组件,放在侧栏(宽 280px)和放在主区(宽 800px)时应该有不同的内部布局。容器查询正是为此而生:

.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 480px) {
  .card {
    flex-direction: row; /* 宽容器:图文左右排列 */
    align-items: center;
  }
}

@container card (max-width: 479px) {
  .card {
    flex-direction: column; /* 窄容器:图文上下排列 */
  }
}

容器查询在现代浏览器中已经广泛支持。它让组件真正具备了“自适应”能力,而不是依赖外部环境猜测。

7.4 逻辑属性与书写模式

在支持多语言的站点中,要注意 flex-direction: row 的“起点”是逻辑上的行内起点,会随书写模式变化。在阿拉伯语、希伯来语等 RTL 语言下,row 会自动从右向左排列,flex-start 也会自动映射到右侧。这是 Flexbox 相对浮动布局的一大优势——你不需要为 RTL 单独写一套样式。

但要注意:margin-left / margin-right 这类物理属性不会自动翻转,应该改用 margin-inline-start / margin-inline-end。

八、Flexbox 与 Grid:不是竞争,而是分工

这是面试里出现频率极高的问题。网上有很多“Flexbox vs Grid”的对比,但真实项目里,它们从来不是二选一的关系。

8.1 本质差异

维度 Flexbox Grid
维度 一维(行 或 列) 二维(行 和 列)
设计思路 内容驱动:先有内容,再决定怎么排 布局驱动:先画格子,再往里放内容
尺寸来源 由子项内容和 flex 属性共同决定 由轨道定义(fr、px、auto)决定
对齐能力 主轴 + 交叉轴 行轴 + 列轴,且可跨行跨列
典型用途 导航、工具栏、表单行、卡片内部 页面骨架、仪表盘、图片墙
子项换行 需要显式声明 flex-wrap 自动形成网格轨道

8.2 一个判断标准

当你面对一个布局需求时,问自己一个问题:

我需要控制的“线”是一条,还是两条?

  • 只需要控制一条线(一行按钮、一列菜单)→ Flexbox
  • 需要同时控制行和列(整页骨架、卡片矩阵)→ Grid
  • 不确定 → 先用 Flexbox,遇到瓶颈再换 Grid。它的学习成本更低,调试更直观。

8.3 组合使用才是常态

一个真实的商品列表页可能是这样的结构:

/* 页面骨架:Grid 负责二维分区 */
.page {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
  grid-template-columns: 240px 1fr;
  gap: 24px;
}

/* 顶部导航:Flex 负责一维排列 */
.header-inner {
  display: flex;
  align-items: center;
  gap: 20px;
}

/* 商品列表:Grid 负责二维排列 */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 20px;
}

/* 单张商品卡内部:Flex 负责一维排列 */
.product-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.product-card__actions {
  display: flex;
  gap: 8px;
  margin-top: auto; /* 按钮吸底 */
}

这种“Grid 搭骨架、Flex 排内容”的模式,是现代 CSS 布局最主流的实践。

九、兼容性、调试与性能

9.1 兼容性现状

Flexbox 的现代语法(无前缀)从 2013 年前后开始在主流浏览器落地。今天,所有你还在支持的浏览器都完整支持它。

特性 支持情况 使用建议
核心属性(direction / justify / align / flex) 全平台 放心使用
gap(弹性容器) 广泛支持 放心使用
flex-basis: content 较新 可用 auto 替代
safe / unsafe 对齐 部分支持 仅作增强,不要依赖
place-content 简写 广泛支持 放心使用
逻辑属性(margin-inline 等) 广泛支持 多语言站点推荐使用
容器查询 现代浏览器 作为渐进增强

9.2 历史上的前缀与旧语法

如果你在维护一个很老的项目,可能会看到这样的代码:

/* 2011 年的旧语法,早已被废弃,不要在新项目中使用 */
.old {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-box-pack: center;
  -webkit-box-align: center;
}

这是 WebKit 早期的实验性语法,属性名和取值都和现在不同(box-pack 对应 justify-content,box-align 对应 align-items)。看到它就知道这段代码至少是十年前写的。除非有明确的旧版兼容需求,否则不要模仿。

9.3 用 DevTools 调试 Flexbox

现代浏览器的开发者工具对 Flexbox 有极好的支持,掌握这几个功能能大幅提升调试效率:

  • flex 徽章:在 Elements 面板中,凡是弹性容器元素旁边都会显示一个小小的 flex 标签,一眼就能定位容器。
  • 点击徽章高亮:点击这个标签会给页面上所有弹性项目加上彩色覆盖层,容器本身的轮廓也会被描出。
  • Layout 面板:在右侧的 Layout 标签页里,可以查看并调整 flex-direction、justify-content、align-items 等属性,实时看到效果。
  • 尺寸标注:选中弹性项目时,DevTools 会显示它的 flex-basis、实际宽度、以及是否被收缩。
  • badge 上的数字:某些版本会在 flex 徽章上显示项目数量,快速判断结构。

9.4 性能:Flexbox 会不会拖慢渲染

结论先行:正常使用下,Flexbox 的性能开销可以忽略不计。它是浏览器原生实现的布局算法,比用 JS 计算位置快得多。

但有几个场景值得注意:

  • 深层嵌套:弹性的计算是有传递性的,一个项目尺寸变化会触发父容器的重新布局,层层向上传播。嵌套五层以上的弹性容器,在频繁变化尺寸时可能带来可感知的卡顿。
  • 频繁改变 flex-basis:这会触发布局重排(reflow)。如果只是改变视觉位置,优先用 transform。
  • 大量项目 + 频繁重排:几百个弹性项目在动画中同时改变尺寸,可能触发布局抖动。此时考虑用 contain: layout 隔离影响范围。
/* 隔离布局影响,减少重排传播 */
.card {
  contain: layout style;
}

9.5 常见渲染问题排查

项目被压扁 检查 flex-shrink,设为 0 或加 min-width
省略号不生效 给收缩项加 min-width: 0
align-content 没反应 确认 flex-wrap: wrap 且确实产生了多行
stretch 不生效 检查项目是否设了交叉轴方向的具体尺寸
居中内容被裁掉 用 safe center 或改用 margin: auto
gap 与 margin 叠加 二选一,不要同时使用

十、无障碍:Flexbox 特有的三个陷阱

Flexbox 极大地提升了布局能力,但也引入了几个全新的可访问性问题。这些问题在浮动时代并不存在,因此格外容易被忽视。

10.1 order 会打乱键盘 Tab 顺序

这是最严重的一个。order 只改变视觉顺序,不改变 DOM 顺序。而键盘的 Tab 键顺序、屏幕阅读器的朗读顺序,都是严格按照 DOM 顺序进行的。

后果是:视觉上第一个的按钮,可能要到 Tab 按三次之后才能聚焦到;屏幕阅读器读到的内容和眼睛看到的顺序完全对不上。

反例
DOM 顺序:取消按钮 → 确认按钮
CSS:.confirm { order: -1 }
视觉上“确认”在左,但 Tab 先聚焦到“取消”。用户按回车会触发错误的操作。
正解
调整 DOM 顺序,让源码顺序与视觉顺序一致。如果确实需要在不同屏幕尺寸下改变顺序,用媒体查询配合 DOM 结构设计,而不是依赖 order。

如果确实无法避免使用 order,请确保被重排的元素之间没有需要按顺序操作的表单控件或链接。纯展示性的元素(如图标、装饰块)重排是无害的。

10.2 视觉顺序与阅读顺序不一致

屏幕阅读器用户在“按标题浏览”时,得到的是 DOM 顺序的标题列表。如果标题的视觉位置通过 flex-direction: column-reverse 或 row-reverse 反转了,用户听到的顺序会和页面上看到的顺序相反。

常见的高危场景包括:

  • 用 column-reverse 实现聊天列表(新消息在下方)——但屏幕阅读器会先读最新的,再读旧的,逻辑上还算合理。
  • 用 row-reverse 实现 RTL 布局——这种场景下应该用 dir="rtl" 属性,而不是反转弹性方向。
  • 用 order 把重要操作提到前面——如果它本来就是第一个,就不需要 order。

10.3 焦点顺序与被裁剪内容

当弹性项目因为空间不足而被 overflow: hidden 裁掉时,里面的链接和按钮仍然可以被 Tab 聚焦,但用户看不到焦点在哪——这是一个非常糟糕的体验。

解决办法是使用 overflow: auto 让容器可滚动,或者用 visibility: hidden / display: none 真正地移除不可见内容(注意后两者会同时移除可聚焦性,这是想要的效果)。

/* 不可见但会被聚焦 —— 危险 */
.danger { overflow: hidden; height: 0; }

/* 真正移除可聚焦性 —— 安全 */
.safe { display: none; }

/* 保留给屏幕阅读器、视觉上隐藏 —— 用于标签等场景 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

10.4 三个零成本的无障碍检查

  1. 拔掉鼠标,只用 Tab 键走一遍页面,确认焦点顺序与视觉顺序一致。
  2. 打开屏幕阅读器(macOS 的 VoiceOver 用 Cmd+F5,Windows 用 Narrator),用标题或地标跳转的方式浏览一遍。
  3. 把所有内容缩小到 200% 缩放,检查弹性容器是否产生了溢出或内容重叠。

十一、十四个高频误区

误区 1 认为 flex: 1 就是“等宽”——它其实是 1 1 0%,会忽略内容宽度
误区 2 忘了 flex-wrap: wrap,以为项目会自动换行
误区 3 用 align-content 去对齐单行项目——单行时它完全无效
误区 4 用 order 重排按钮,导致 Tab 顺序与视觉顺序不符
误区 5 忘记 min-width: 0,导致省略号不生效、内容溢出
误区 6 同时使用 gap 和 margin 做间距,导致间距翻倍
误区 7 在只需要一维排列时硬套 Grid,或反之
误区 8 给弹性项目设 float 或 vertical-align,期待它们生效
误区 9 用 justify-content: space-between 处理动态数量的项目,最后一行错位
误区 10 把 height: 100% 写在弹性项目上,期待它填满交叉轴——应该用 stretch
误区 11 用 flex-basis 百分比时不考虑 gap 占用的空间,导致换行
误区 12 在打印样式里保留 flex-shrink: 0,导致内容超出页面宽度
误区 13 认为弹性容器内的 margin 会合并——它们永远不会合并
误区 14 忘记弹性容器的直接子元素才是弹性项目,嵌套的孙子元素不受影响

三个需要展开说的点

关于误区 9:space-between 与动态数量

justify-content: space-between 在项目数量会变化时有个经典问题:如果最后一行只有两个项目,它们会被推到两端,与上一行的排列完全对不上。此时应该用 gap 配合 justify-content: flex-start,或者直接改用 Grid。

关于误区 10:height: 100% 的失效

弹性项目在交叉轴方向已经被 align-items: stretch 处理了。如果你手动设 height: 100%,会覆盖 stretch 行为,而且百分比高度依赖父元素有明确高度,很容易失效。要填满交叉轴,什么都不做就是最好的做法。

关于误区 11:flex-basis 与 gap 的关系

当容器有 gap: 20px 且一行放 3 个项目时,可分配给项目的宽度是 容器宽度 − 40px。如果你设 flex-basis: 33.33%,那么三项相加就是容器宽度的 99.99%,再加上 40px 的 gap,必然溢出换行。正确的做法是让 flex 自己分配:flex: 1 1 0 或 flex: 1 1 240px。

十二、Flexbox 检查清单与代码基线

把全文的结论浓缩成一份可以贴在工位上的清单。每次写完布局扫一眼,能挡掉绝大多数问题。

  • 先判断维度:一维用 Flex,二维用 Grid,不要混用。
  • 容器写在父级:display: flex 只影响直接子元素,孙子元素需要自己的容器。
  • 默认不换行:需要换行必须显式写 flex-wrap: wrap。
  • 对齐分工明确:justify-content 管主轴,align-items 管交叉轴,align-content 只管多行分布。
  • 间距优先用 gap:不要用 margin 加负边距的老办法。
  • 弹性基准想清楚:flex-basis: 0 是等分,flex-basis: auto 是先内容后分剩余。
  • 简化 flex 简写:flex: 1 / flex: auto / flex: none 覆盖 90% 的场景。
  • 固定尺寸的项目写 flex: none:图标、头像、按钮。
  • 收缩项加 min-width: 0:需要省略号或长文本收缩时必备。
  • 避免用 order 重排交互元素:DOM 顺序就是无障碍顺序。
  • auto 外边距是好工具:把某一组元素推到另一端,比嵌套容器更简洁。
  • 居中用 place-content:display: flex; place-content: center;
  • 页脚吸底用 margin-top: auto:比给 main 设 flex: 1 更简洁。
  • 不要给弹性项目设 float / vertical-align:它们不会生效。
  • 打印样式单独处理:移除固定宽度和禁止收缩的设置。
  • 用 DevTools 的 flex 徽章:快速定位容器与项目。
  • 只按 Tab 键走一遍页面:确认焦点顺序与视觉顺序一致。
  • 放大到 200% 检查溢出:确保极端情况下内容不会重叠。
/* ===== 一份可直接复用的 Flexbox 工具集 ===== */

/* 水平垂直居中 */
.u-center {
  display: flex;
  place-content: center;
}

/* 两端对齐的水平排列 */
.u-row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* 垂直堆叠 */
.u-stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 自动换行的等宽网格 */
.u-auto-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.u-auto-grid > * {
  flex: 1 1 240px;
  min-width: 0;
}

/* 侧栏固定 + 主区自适应 */
.u-sidebar-layout {
  display: flex;
  gap: 32px;
  align-items: flex-start;
}
.u-sidebar-layout > .aside { flex: 0 0 280px; }
.u-sidebar-layout > .main { flex: 1; min-width: 0; }

/* 文本溢出省略号(弹性项目内) */
.u-truncate {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 弹性项目中的自动外边距工具 */
.u-push-end { margin-inline-start: auto; }
.u-push-start { margin-inline-end: auto; }

/* 全局兜底:防止弹性项目因 min-width: auto 溢出 */
:where(.u-auto-grid, .u-sidebar-layout) > * {
  min-width: 0;
}

Flexbox 最迷人的地方在于它的表达力与简洁性的平衡。一个“左中右三段式、两端对齐、垂直居中、间距 16px”的导航栏,用 Flexbox 只需要五行 CSS;在浮动时代,这需要十几个属性加上两个 hack,还得反复调浏览器差异。

但简洁不等于简单。flex: 1 和 flex: auto 看起来只差一个单词,行为却完全不同;min-width: auto 这个默认值,能让你的省略号彻底失效;order 用起来很爽,代价是键盘用户的困惑。真正的“完全指南”,不是背下所有属性,而是在写下每一行 CSS 时,知道它背后发生了什么。

所以,下一次当你准备给一个容器加上 display: flex 的时候,不妨先问自己三个问题:“我要排的是一维还是二维?主轴应该朝哪?空间不够时我希望谁让步?”——想清楚这三个问题,Flexbox 就不再是一堆需要记忆的属性,而是一种可以推理的布局语言。