为什么现代布局绕不开 Flex 与 Grid
十年前写页面,很多人的第一反应是 float 加清除浮动,再用负外边距和绝对定位去「挪」元素。这类方案能出效果,但代价是:布局意图被藏在补偿性的样式里,改一处、塌一片,接手的人不敢动。Flex 与 Grid 改变的正是这件事——它们把「怎么排」从技巧变成了语言。
Flex(弹性盒子,CSS Flexible Box Layout)解决的是一维排布:一排菜单、一列表单、一组按钮、一个底部操作栏。Grid(网格布局)解决的是二维排布:既有行又有列的页面骨架、卡片墙、仪表盘、后台管理的三栏布局。两者不是替代关系,而是分工关系——这一点想通了,后面的选择就不会纠结。
一句话记住它们的定位:Flex 管一条线,Grid 管一个面。当你需要「把一串东西按某个方向摆开并分配空间」时用 Flex;当你需要「先划好格子,再把内容放进指定格子」时用 Grid。
不要再用浮动去实现布局了。浮动是文字环绕的排版工具,弹性盒子与网格才是布局工具。把旧习惯换掉,CSS 代码量通常会减少一半。
- 主轴与交叉轴Flex 的一切对齐都围绕这两条轴展开,方向由 flex-direction 决定。
- flex 简写flex: grow shrink basis,理解 basis 才能预判元素宽度。
- 网格轨道Grid 先定义列与行(track),再用网格线把内容放进去。
- minmax 与 auto-fill一条声明就能实现不依赖媒体查询的响应式卡片墙。
Flex 主轴与交叉轴:一维排列的思维模型
给容器加上 display: flex 后,它的直接子元素会变成「弹性项目」,并沿一条主轴排列。主轴方向由 flex-direction 决定:row(默认,横向)或 column(纵向)。与主轴垂直的称为交叉轴。所有对齐属性都只作用在这两条轴上,这是 Flex 最容易被误解的地方。
关键映射关系是:justify-content 控制主轴方向的对齐与分配,align-items 控制交叉轴方向的单行对齐,align-content 只在换行后作用于多行整体。把这三个属性和两条轴对起来,就再也不会记错谁管横谁管竖。
| 属性 | 作用 | 常用值 |
|---|---|---|
| flex-direction | 决定主轴方向,其余属性都随之改变含义 | row / column / row-reverse |
| justify-content | 主轴上的对齐与剩余空间分配 | flex-start / center / space-between / space-evenly |
| align-items | 交叉轴上的对齐方式 | stretch / center / flex-start |
| gap | 项目间距,不产生边缘多余空隙 | 任意长度,如 12px / 1rem |
| flex-wrap | 主轴空间不足时是否换行 | nowrap / wrap |
项目一侧还有三个属性值得单独记:flex-grow(空间富余时是否放大)、flex-shrink(空间不足时是否收缩)、flex-basis(初始尺寸)。它们的简写 flex 决定了「谁更大、谁更小」。日常最常用的两档是 flex: 1(可放大可收缩、基准为 0,用来瓜分剩余空间)与 flex: 0 0 auto(完全按内容尺寸,不参与分配)。
- flex: 1 让所有这类项目等宽平分,是工具栏、分栏容器的主力写法。
- flex: 0 0 auto 用来保护图标、头像、按钮不被压扁。
- min-width: 0 是防溢出补丁:弹性项目默认最小尺寸为内容宽度,长英文单词会把容器撑破,加它即可让文本正常换行。
Flex 实战:导航、卡片与垂直居中
弹性盒子的价值在于把「老办法要写一堆辅助类」的事变成几行声明。最常见的三个场景是顶部导航两端对齐、卡片底部按钮分列两侧、以及弹窗垂直居中。下面这段 CSS 可以直接拿去用,也可以放进任意项目的公共样式里。
/* 顶部导航:两端对齐 */
.site-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-nav .links {
display: flex;
gap: 12px;
flex: 1;
min-width: 0; /* 防长文本撑破容器 */
}
/* 万能垂直居中:三行搞定 */
.modal-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 60vh;
}
垂直居中曾经是 CSS 的经典难题,现在它是 display: flex 加两行对齐声明的事。原因在于 justify-content: center 把主轴上的剩余空间平均分到两侧,align-items: center 在交叉轴上做同样的事,两个方向一起居中,效果就出来了。
几个容易踩的细节值得单独记住:
- 间距优先用 gap,而不是给每个项目加 margin。margin 会出现在首尾元素外侧,需要
:last-child特殊处理,gap 不会。 - 需要换行时显式写 flex-wrap: wrap。默认不换行,项目会被压缩到变形。
- 「最后一个元素靠右」用 margin-left: auto。给该项目加这一条,它会吃掉主轴上的所有剩余空间,比绝对定位干净得多。
- 等高列免费得到。交叉轴默认 stretch,同一行的项目自动等高,这正是很多「卡片高度不一致」问题的解药。
Grid 网格线:二维布局的表达力
Grid 的思路与 Flex 完全不同:先在容器上划出轨道(行与列),再把子元素放进指定的格子。轨道之间的分界称为网格线,从 1 开始编号;因此 grid-column: 1 / 7 表示「从第 1 条列线跨到第 7 条列线」,也就是占 6 列。
定义轨道用 grid-template-columns 与 grid-template-rows,单位 fr 表示「剩余空间的一份」。与百分比不同,fr 会自动扣除 gap 再分配,不会因为间距而溢出;配合 minmax(0, 1fr) 还能防止长内容顶破轨道。
/* 一个 12 列、列间距 24px 的网格 */
.grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 24px;
}
/* 用网格线指定位置:从第 1 条列线跨到第 7 条 */
.main-col { grid-column: 1 / 7; }
.side-col { grid-column: 7 / 13; }
/* 命名区域,可读性最高 */
.layout {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
grid-template-areas: "side main" "side foot";
gap: 24px;
}
.layout .side { grid-area: side; }
.layout .main { grid-area: main; }
.layout .foot { grid-area: foot; }
三处值得一提的能力:其一,grid-template-areas 用字符串画出布局草图,可读性远高于一堆行列数字,适合页面级骨架;其二,没有显式定位的子元素会按 grid-auto-flow 自动填充,卡片墙就是靠它自动换行;其三,Grid 的 gap 同样不产生边缘空隙,这是它比 margin 方案更省心的原因之一。
Grid 实战:响应式卡片墙与页面骨架
「不写媒体查询也能自适应」是 Grid 最实用的特性,靠的是 repeat(auto-fill, minmax(240px, 1fr)):浏览器尽可能多地塞入宽度不小于 240px 的列,多余空间再平摊,容器变窄时列数自动减少。它把过去三四个断点的媒体查询压缩成一行。
/* 响应式卡片墙:无需媒体查询 */
.card-wall {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
/* 卡片内部仍用 Flex 处理一行按钮 */
.card-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
注意倒数第二行那行注释点出的组合关系:页面级用 Grid 划骨架,组件级用 Flex 排一行。这不是规定,而是让代码最省心的默认分工——外层关心区域划分,内层关心顺序与对齐,两套语义各管一段。
auto-fill 与 auto-fit 常被混用,差别在空轨道是否保留:auto-fill 保留空列,卡片数量少时仍按最小宽度排列、不会拉得过宽;auto-fit 折叠空轨道,让剩余卡片撑满整行。列表页希望保持卡片尺寸稳定,选 auto-fill;希望「一张卡片也铺满」时选 auto-fit。
- 卡片墙用
auto-fill,保持视觉节奏一致。 - 单列阅读型内容用
minmax(0, 720px)加justify-content: center限宽居中。 - 骨架布局用
grid-template-areas,媒体查询里只改面积定义即可切换为单列。
Flex 与 Grid 如何组合
把两者对立起来是常见误区。真实项目里它们几乎总是同时出现:一个后台管理页,外层 Grid 划分侧边栏与主区,主区内部 Grid 铺卡片墙,每张卡片的标题行用 Flex 两端对齐,表单字段用 Flex 纵向排列。它们不是二选一,而是各自解决自己最擅长的那一层。
| 维度 | Flex | Grid |
|---|---|---|
| 布局维度 | 一维:主轴 + 交叉轴 | 二维:行与列同时定义 |
| 尺寸来源 | 由内容与剩余空间分配 | 由轨道定义,可精确到网格线 |
| 对齐方式 | 主轴一条、交叉轴一条 | 每个格子可独立对齐 |
| 典型场景 | 导航、工具栏、按钮组、垂直居中 | 页面骨架、卡片墙、仪表盘、表单栅格 |
| 学习曲线 | 概念少,上手快 | 轨道与网格线稍抽象,表达力更强 |
决策上的经验规则是:当元素的排列顺序由内容决定、数量不固定时用 Flex;当布局由设计稿决定、需要行列对齐时用 Grid。一条导航里几个菜单项,数量可变、只排一排,用 Flex;一个「左侧栏 + 主区 + 底部」的固定骨架,行列明确,用 Grid。反过来用也能实现,但代码会别扭,维护成本更高。
还有一个容易被忽略的点:Grid 的子元素也可以是 Flex 容器,反之亦然,嵌套不受限制。真正需要克制的只有一件事——不要为了「看起来高级」而在一个区域里叠三四层网格,深嵌套会让响应式调整变得难以推理。
兼容性、性能与常见踩坑
Flex 的支持可以追溯到 IE11(部分行为有差异),Grid 在 IE 上则是带前缀的旧语法,与今天标准差别很大。如果项目必须兼容 IE,常规做法是渐进增强:先给一个块级降级布局保证可读,再用 @supports 检测到支持 Grid 时启用网格增强。绝大多数现代项目已经不需要这层兜底,但知道思路能让你在评审时说得清取舍。
/* 渐进增强:老浏览器退化为单列,不影响可读性 */
.grid {
display: block;
}
@supports (display: grid) {
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 20px;
}
}
性能方面,布局本身的开销通常不构成瓶颈,真正拖慢渲染的是这些写法:
- 用 grid-template-rows 做动画。轨道尺寸变化会触发布局重算,动画请改用 transform 或 opacity。
- 深层嵌套 Grid 加大量 auto 轨道。自动轨道需要二次测量,长列表里开销明显,能给定值就给定值。
- 频繁读写布局属性。在循环里读
offsetHeight再写样式会引发布局抖动,读一次缓存、写一次提交更好。 - 忘记 min-width: 0。弹性项目里放一个长 URL 或长单词,容器会被直接撑破,表现为横向滚动条。
另外三个高频踩坑:align-items 与 align-content 弄混(前者管单行、后者管换行后的多行整体);height: 100% 在没有确定高度的父容器里失效(改用 min-height: 100dvh 之类的视口单位更可靠);以及把 gap 与 margin 混用导致间距翻倍。这些问题都不会报错,只会让页面「看起来差一点」。
一套可复用的布局自查清单
写完一个页面,按下面这份清单过一遍,能拦下大部分返工:
- 方向对不对:需要行列对齐的区域用 Grid,单方向排列的用 Flex,没有混用错。
- 间距对不对:统一用 gap,没有残留的 margin 补偿;末行没有多余的空白。
- 压缩对不对:图标、头像、按钮有
flex: 0 0 auto,不会被压扁变形。 - 溢出对不对:长文本容器有
min-width: 0,窄屏无横向滚动条。 - 窄屏对不对:320px 宽度下仍可读,多列能正确塌缩为单列。
- 降级对不对:极端内容(超长标题、空数据)下布局不崩。
布局代码的质量,不体现在「能摆出设计稿」,而体现在「换一段内容、换一个宽度,它依然站得住」。把这份清单固化成习惯,比记住更多属性更有价值。
最后回到起点:Flex 和 Grid 之所以值得花时间学,不是因为它们新,而是因为它们把布局意图直接写进了代码。justify-content: space-between 一眼就读出「两端对齐」,而过去的浮动加清除浮动,需要读三层样式才能还原意图。下一篇我们会转向 DOM 与事件——把注意力从「怎么排」移到「怎么动」。