CSS Grid 在 2017 年随主流浏览器全面落地,到今天已经不再是“新特性”,而是每一个前端工程师都必须掌握的布局基础设施。但真正把它用透的人并不多——大多数人停留在 display: grid 加 grid-template-columns: repeat(3, 1fr) 这一层,遇到稍微复杂的场景就退回 Flexbox 加一层层嵌套 div。本篇 45 分钟长文,从 Grid 与 Flexbox 的分工讲起,把术语体系、轨道尺寸单位、fr 的分配算法、自动放置规则、对齐系统、网格线定位、命名区域、响应式三件套、Subgrid、性能陷阱、可访问性一路拆到底,中间穿插十二个可以亲手拖拽的交互演示,最后用两个完整实战和一份速查清单收尾。读完你不仅能写出更少的 CSS,还能明白每一行 CSS 到底在做什么。
零、为什么 Grid 值得你投入时间
在开始讲语法之前,先回答一个更根本的问题:Flexbox 已经能解决大部分布局问题了,为什么还要学 Grid?
答案不是“Grid 更强大”,而是“它们解决的是不同维度的问题”。下面这个页面骨架,如果用 Flexbox 实现,需要至少三层嵌套容器;用 Grid,一个元素就够了:
.app { display: flex; flex-direction: column; }
.app__body { display: flex; flex: 1; }
.app__sidebar { width: 240px; }
.app__main { flex: 1; }
/* Grid 版:一个容器搞定 */
.app {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100dvh;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
grid-template-areas 用 ASCII 画图的方式描述布局,可读性极高
浏览器支持:已经不需要担心
| 特性 | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| 基础 Grid | 57+ | 52+ | 10.1+ | 16+ |
gap 简写 |
66+ | 61+ | 12+ | 79+ |
| Subgrid | 117+ | 71+ | 16+ | 117+ |
| 容器查询 | 105+ | 110+ | 16+ | 105+ |
dvh / svh |
108+ | 101+ | 15.4+ | 108+ |
可以看到,除 Subgrid 和容器查询需要较新版本外,Grid 的核心能力早已全面可用。即便需要兼容旧浏览器,也有成熟的降级方案——通常就是回退到 Flexbox 或浮动布局,不影响主流程。
学 Grid 的投入产出比
学习 Grid 的成本大约是一周:两天理解核心概念,三天练习常用模式,两天在真实项目里替换掉旧的嵌套布局。而收益是长期的——每次写新页面时,你都会比之前少写 30% 到 50% 的布局代码,而且这些代码更容易读懂、更容易改。
如果你还在犹豫,可以先用一个小场景试水:把项目里最复杂的那个“三栏加通栏”的布局用 Grid 重写一遍,感受一下差异。大多数人试过一次之后,就再也不想回到嵌套 Flexbox 了。
一、Grid 与 Flexbox:不是替代,是分工
很多教程喜欢把 Grid 说成“Flexbox 的升级版”,这个说法非常容易误导人。它们解决的是不同层次的问题,而且经常出现在同一段代码里。
一维与二维的分界线
Flexbox 是一维布局:它一次只关心一个方向上的排列。你设置 flex-direction: row,它就在水平方向排布,交叉轴上的对齐只能通过 align-items 做粗略控制;你想让第二行某个元素和第一行的元素对齐,就得嵌套一层新的 flex 容器。
Grid 是二维布局:行和列同时被定义。每个子元素的位置由“行 + 列”两个坐标共同决定,你可以让 A 横跨两列、B 纵跨两行,而它们仍然共享同一套对齐基准线。
| 对比维度 | Flexbox | CSS Grid |
|---|---|---|
| 维度 | 一维(行或列) | 二维(行和列) |
| 尺寸决策者 | 内容驱动,容器让位于内容 | 容器驱动,内容让位于轨道 |
| 布局方向 | 由 flex-direction 决定 |
行列同时存在,无“方向”概念 |
| 是否支持重叠 | 不支持(需负边距等技巧) | 原生支持,同一格子可放多个元素 |
| 典型场景 | 导航条、按钮组、卡片内部 | 页面骨架、卡片列表、仪表盘 |
| 子元素排序 | order |
order + 显式网格位置 |
| 学习曲线 | 较平缓,属性少 | 较陡,概念体系更完整 |
一个实用的选择法则
- 你需要同时控制行和列
- 布局结构是“先有框架,再往里填内容”
- 需要让某些元素跨越多个单元格
- 需要元素之间精确对齐到同一套基准线
- 需要元素重叠(比如图片上叠文字)
- 只有一行或一列,且不需要跨行跨列
- 内容长度不定,希望容器跟着内容走
- 需要
flex-grow那种“按内容比例分配”的行为 - 是组件内部的小结构,嵌套 Grid 反而增加认知负担
它们其实是最好的搭档
真实的页面往往是这样的:外层用 Grid 定义页面骨架,每个区域内部用 Flexbox 排列小元素,卡片列表用 Grid 做响应式,卡片内部又用 Flexbox 排列标题和按钮。
.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
/* 内层:Flexbox 负责一维排列 */
.card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
记住这句话:Grid 管“面”,Flexbox 管“线”。需要二维定位就用 Grid,只需要沿一条轴线排队就用 Flexbox。不要因为 Grid 更强大就到处用它,也不要因为熟悉 Flexbox 就永远不用 Grid。
二、术语体系:先把名词搞清楚
Grid 的 API 看起来很多,但绝大多数都是围绕少数几个核心概念展开的。把这些名词搞明白,后面所有属性都会变得顺理成章。
display: grid 或 display: inline-grid 的元素
grid-area 引用
grid-template-* 明确声明出来的行列
gap / row-gap / column-gap,只作用于轨道之间,不作用于容器边缘
一个必须记住的细节:只有直接子元素才是网格项
这是新手最容易踩的坑。下面的写法中,.item 是网格项,而 .inner 不是:
<div class="item">
<div class="inner">我不是网格项</div>
</div>
</div>
/* 网格项在网格里自动拉伸,但它的内容不会 */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); }
.item { /* 会自动填满所在单元格 */ }
所以当你发现“某个元素明明在网格里却不参与布局”,第一件事就是检查它是不是容器的直接子元素。如果不是,要么调整 DOM 结构,要么给它加上 display: contents(慎用,见后面的陷阱一节)。
网格线编号:正数从前,负数从后
/* 1 2 3 4 5 */
/* | 列1 | 列2 | 列3 | 列4 | */
/* -5 -4 -3 -2 -1 */
/* 下面两种写法完全等价:都占据最后一列 */
.a { grid-column: 4 / 5; }
.b { grid-column: -2 / -1; }
负号索引的价值在于不需要知道总列数。当你写一个组件时,如果它需要“贴右边”,用 -2 / -1 就比写死 12 / 13 更健壮——别人把这个组件放进不同列数的网格里,它依然正确。
三、轨道尺寸单位家族:远不止 1fr
grid-template-columns 和 grid-template-rows 接受的值远比大多数人想象的丰富。下面这张“条形图”直观地展示了各种单位在分配空间时的行为差异。
120px固定长度,不随容器变化25%相对容器宽度,容易出现小数像素1fr剩余空间的一份,随容器伸缩auto由内容决定,但可以被拉伸填满min-content内容能排下所需的最小宽度max-content内容不换行时的完整宽度fit-content(240px)介于 min-content 与上限之间逐个拆解
固定长度(px / rem / em / vw…):最容易理解,但也最容易在响应式场景下出问题。grid-template-columns: 300px 1fr 在窄屏上会导致横向溢出,除非配合媒体查询或 minmax()。
百分比:相对于容器的内容盒宽度计算。看起来直观,但它不能和 gap 和平共处——如果你写了三列 33.33%,再加上两个 gap,总宽度就超过 100% 了。这正是 fr 存在的意义。
fr(fraction):网格布局引入的弹性单位,表示“剩余空间的一份”。它自动把 gap 扣除掉之后再分配,所以永远不会溢出。
auto:这个值有两副面孔。在轨道尺寸里,它表示“由内容决定,但如果有剩余空间也可以被拉伸”;在自动放置时,它又表示“自动分配位置”。它和 1fr 的区别在于:1fr 会无脑均分剩余空间,而 auto 会先照顾内容尺寸。
min-content / max-content:前者是“内容在最窄情况下需要的宽度”(通常是其中最长的那个不可断词),后者是“内容完全展开、不换行时的宽度”。它们常被用来做“智能宽度”的列:
.table-like {
display: grid;
grid-template-columns: max-content 1fr;
gap: 16px;
}
fit-content(limit):可以理解为 min(max-content, max(min-content, limit))。翻译成人话就是:“尽量贴合内容,但不要超过 limit”。用它做侧边栏非常合适:
display: grid;
/* 侧边栏最多 280px,内容少时可以更窄 */
grid-template-columns: fit-content(280px) 1fr;
gap: 24px;
}
minmax():给轨道加上下限和上限
minmax(min, max) 是响应式 Grid 的基石。它定义了一条轨道的尺寸范围:
.layout {
grid-template-columns: minmax(200px, 25%) 1fr;
}
/* 卡片最小 240px,最大填满可用空间 */
.cards {
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
/* 内容列最小 0,最大 1fr —— 解决溢出问题的经典配方 */
.content {
grid-template-columns: minmax(0, 1fr);
}
repeat() 的两种写法
grid-template-columns: repeat(3, 1fr);
/* 等价于:1fr 1fr 1fr */
/* 写法二:自动填充(auto-fill / auto-fit) */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
/* 可以混用:第一列固定,后面三列平分 */
grid-template-columns: 80px repeat(3, 1fr);
/* 多个轨道重复:先 1fr,再 2fr,再 1fr,循环两次 */
grid-template-columns: repeat(2, 1fr 2fr 1fr);
动手试试:列数与间距
拖动下面的滑块,观察列数和间距如何实时改变布局。注意每一列始终是等宽的,且总宽度永远等于容器宽度减去间距——这正是 fr 的分配结果。
四、fr 的分配算法:为什么它比百分比聪明
fr 看起来只是“等分”,但它的计算规则比想象中细致。理解这套规则,能帮你解释很多“为什么我的布局长这样”的疑惑。
三步分配法
- 先扣除固定项:把
px、auto、min-content这类非弹性轨道按各自规则确定尺寸,同时扣除所有gap。 - 剩下的才是“剩余空间”:这个剩余空间就是所有
fr轨道要瓜分的总量。 - 按 fr 系数比例分配:
1fr 2fr意味着第一列拿 1/3,第二列拿 2/3。
动手算一算
下面这个计算器可以帮你验证 fr 的分配结果。输入容器宽度、间距和列定义,它会实时算出每一列的实际宽度。
注意 gap 是被优先扣除的,这就是为什么用 fr 永远不会溢出,而用 33.33% 加 gap 会。
fr 的最小值陷阱:为什么需要 minmax(0, 1fr)
这是 Grid 最经典的坑。考虑下面这个场景:网格里放了一个很长的不可断词的 URL,或者一个 overflow-x: auto 的表格。
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
/* ✅ 修复:给轨道设一个 0 的下限 */
.grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
原因:1fr 其实等价于 minmax(auto, 1fr),而 auto 作为最小值时会取 min-content。也就是说,轨道不可能小于它内容的最小宽度。当内容的最小宽度大于均分值,轨道就被撑大了,整个网格随之溢出。
把它显式改成 minmax(0, 1fr),最小值变成 0,轨道就可以被压缩,超出的内容交给 overflow 去处理。
minmax(0, 1fr)。养成习惯后能省下大量调试时间。
overflow: hidden 没生效,或者是子元素宽度写死了,排查半天才发现是 1fr 的最小值在作祟。
五、自动放置:grid-auto-flow 与 span
当你没有给网格项指定明确位置时,浏览器会按照一套既定算法把它们依次放进网格。这套算法由 grid-auto-flow 控制。
三种流动方向
grid-auto-flow: row;
/* 逐列填充 */
grid-auto-flow: column;
/* 密集模式:回头填补前面留下的空洞 */
grid-auto-flow: row dense;
dense 是一个很有用的选项。默认情况下,如果一个网格项跨了多列,导致前面出现空格,后续的小项目不会回头去填那个空格,而是继续往后排,留下视觉上的“洞”。加上 dense 之后,浏览器会尝试用小项目把洞补上。
span:跨越多个轨道
不给具体的起止线,只声明“跨几格”,叫做 span 定位。它在响应式场景下比写死数字更灵活:
.wide { grid-column: span 2; }
/* 跨两行 */
.tall { grid-row: span 2; }
/* 从第 2 条线开始,跨 3 列 */
.hero { grid-column: 2 / span 3; }
/* 简写:grid-area: row-start / column-start / row-end / column-end */
.feature { grid-area: 1 / 1 / 2 / span 2; }
span 的一个隐藏优点是:当你在媒体查询里改变列数时,跨列的元素不需要跟着改数字。三列时 span 2 是三分之二宽,两列时它就是整行——布局逻辑保持一致。
一个完整的响应式卡片列表示例
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-auto-rows: minmax(180px, auto);
gap: 20px;
}
/* 第一张卡片突出显示:宽两列、高两行 */
.cards > :first-child {
grid-column: span 2;
grid-row: span 2;
}
/* 窄屏下降级为普通卡片,避免撑破布局 */
@media (max-width: 640px) {
.cards > :first-child {
grid-column: auto;
grid-row: auto;
}
}
grid-auto-rows: minmax(180px, auto) 是另一个实用技巧:它让隐式生成的行至少 180px 高,但内容多时可以继续长高,避免卡片高度被硬性截断。
六、对齐系统:三个层级,九个属性
Grid 的对齐属性看着多,其实只需要记住两个维度(行 / 列)乘三个层级(容器内轨道、单元格内项目、单个项目自己)。
层级一:轨道在容器内如何分布(*-content)
只有当轨道总尺寸小于容器尺寸时,这些属性才有效果。常见于用了固定宽度轨道、又没有填满容器的情况。
justify-content: start | center | end | space-between | space-around | space-evenly;
/* 行方向的同一套值 */
align-content: start | center | end | space-between | space-around | space-evenly;
/* 简写:先 align 后 justify */
place-content: center center;
层级二:项目在单元格内如何对齐(*-items)
这是最常用的层级。stretch 是默认值——项目填满整个单元格。
align-items: stretch | start | center | end; /* 垂直 */
place-items: center center; /* 简写 */
层级三:单个项目的自我对齐(*-self)
作用于具体的网格项,优先级高于容器上的 *-items:
justify-self: end;
align-self: start;
}
动手试试:切换对齐方式
点击下面的按钮,观察六个网格项在各自单元格里的位置变化。注意 stretch 会让项目铺满单元格,而其他值会让项目收缩到内容尺寸。
用 Grid 实现完美居中
这是一个被反复提及的场景,用 Grid 只需要三行:
display: grid;
place-items: center;
min-height: 100vh;
}
/* 等价于 Flexbox 的:
display: flex;
align-items: center;
justify-content: center; */
两者都可以,但 Grid 版本少写一行,而且当你有多个子元素需要同时居中时,Grid 能保证它们互相不干扰。
七、网格线定位与命名线
自动放置很方便,但真正的精确控制来自手动定位。你可以用编号,也可以用命名线——后者的可读性高出一大截。
用编号定位
display: grid;
grid-template-columns: 120px 1fr 120px;
grid-template-rows: 64px 1fr 48px;
}
.header { grid-column: 1 / -1; grid-row: 1 / 2; }
.sidebar { grid-column: 1 / 2; grid-row: 2 / 3; }
.main { grid-column: 2 / 3; grid-row: 2 / 3; }
.footer { grid-column: 1 / -1; grid-row: 3 / 4; }
这种写法的可读性不太好——你得在心里数线。而且在网格结构调整后,所有数字都要重新核对。
用命名线定位
在定义轨道时,用方括号给网格线起名字:
display: grid;
grid-template-columns:
[full-start] 1fr [main-start] minmax(0, 720px) [main-end] 1fr [full-end];
}
/* 内容区:从 main-start 到 main-end */
.content { grid-column: main; }
/* 通栏:从 full-start 到 full-end */
.hero { grid-column: full; }
注意 grid-column: main 这种简写——当起止线同名时,浏览器会自动找到这对名字之间的区域。这比写 main-start / main-end 更简洁。
同一个名字可以出现在多条线上。比如 repeat(3, [col-start] 1fr [col-end]) 会生成三组线,此时 grid-column: col-start 2 / col-end 3 表示“第二组 col-start 到第三组 col-end”。
grid-template-areas:可读性最高的方案
如果你追求的是“一眼就能看懂布局”,那 grid-template-areas 是最好的选择。它用 ASCII 画图的方式描述整个页面结构。
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
gap: 12px;
min-height: 100vh;
}
.page > header { grid-area: header; }
.page > aside { grid-area: sidebar; }
.page > main { grid-area: main; }
.page > footer { grid-area: footer; }
几个使用要点:
- 每个字符串代表一行,字符串中的每个词代表一列。
- 同名的单元格会被合并成一个矩形区域。区域必须是矩形,L 形或断开的名字会直接报错。
- 用
.(一个点)表示空单元格:"header . ."。 - 连续的点(
...)表示跨越多列的空位,可以提升对齐可读性。 - 在媒体查询里重写
grid-template-areas,就能整体改变页面结构,其他 CSS 一行都不用动。
@media (max-width: 768px) {
.page {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
这是 Grid 最优雅的地方之一:布局结构集中在一个地方描述,组件本身完全不知道自己在页面哪个位置。这也让 CSS 与 DOM 顺序解耦——移动端可以把侧边栏的 DOM 放在后面,视觉上却排在主要内容之后。
八、响应式 Grid 的三件套
用 Grid 写响应式布局,有三套工具,从简单到复杂依次是:auto-fit/auto-fill、媒体查询、容器查询。
第一件:auto-fit + minmax,无需媒体查询的响应式
这是最令人惊艳的一招。一行 CSS,卡片列表就能在任意宽度下自动决定列数:
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
}
/* 容器 1000px 时:1000 / 240 ≈ 4 列
容器 700px 时:700 / 240 ≈ 2 列
容器 300px 时:1 列 */
auto-fill 与 auto-fit 的区别
这两个关键字只差一个字母,效果却明显不同。下面并排展示它们在同一容器、同一内容下的表现:
auto-fit;需要保持固定列宽、不想让卡片被拉宽时用 auto-fill
第二件:媒体查询,处理结构性变化
auto-fit 只能处理“列数变化”,无法处理“结构变化”(比如侧边栏从左边移到上面)。这类需求仍然要靠媒体查询:
display: grid;
grid-template-columns: 240px 1fr 320px;
gap: 20px;
}
@media (max-width: 1200px) {
.dashboard {
grid-template-columns: 200px 1fr;
}
.dashboard__aside {
grid-column: 1 / -1; /* 变通栏 */
}
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
}
.dashboard > * {
grid-column: auto;
}
}
注意最后那条 grid-column: auto——它把之前设置的跨列覆盖掉,让所有子元素回到自动放置。这是一种很常用的“重置”手法。
第三件:容器查询,组件级的响应式
媒体查询看的是视口宽度,容器查询看的是父容器宽度。这意味着同一个卡片组件,放在宽的主内容区显示为横向布局,放在窄的侧边栏自动变成纵向布局——不需要任何额外的类名。
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
/* 容器宽度超过 420px 时,变成左右两栏 */
@container card (min-width: 420px) {
.card {
grid-template-columns: 160px 1fr;
}
}
容器查询与 Grid 是绝配:Grid 负责内部的二维排布,容器查询负责在不同可用宽度下切换排布方式。两者结合,组件终于做到了真正的“自包含”——它在哪里、长什么样,由它自己的宽度决定,而不是由整个页面决定。
九、Subgrid:让嵌套网格对齐
嵌套网格一直有个痛点:子网格无法继承父网格的轨道。父网格里有三列等宽,子网格想把自己的项目对齐到父网格的列上,只能手动重复一遍尺寸定义——一旦父网格改了,子网格就错位了。
Subgrid 解决了这个问题:
.page {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
}
/* 子网格:直接复用父级的列轨道 */
.page > section {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
/* section 里的直接子元素现在与父网格的列完全对齐 */
Subgrid 还可以只继承行、不继承列,或者相反:
grid-column: span 2;
grid-row: span 3;
display: grid;
/* 行继承父级,列自己定义 */
grid-template-rows: subgrid;
grid-template-columns: repeat(2, 1fr);
}
/* 让卡片内部的头部/主体/底部自动对齐到同一行 */
最典型的应用是“卡片列表里,所有卡片的标题、正文、按钮都对齐到同一水平线”。在没有 Subgrid 的年代,这需要给每一部分固定高度,或者用 JavaScript 测量最大高度。有了 Subgrid,交给浏览器就好。
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
grid-auto-rows: auto;
}
.card {
/* 每张卡片跨 3 行,并把行轨道交给父级决定 */
grid-row: span 3;
display: grid;
grid-template-rows: subgrid;
gap: 0;
}
/* 结果:所有卡片的标题行、正文行、按钮行高度一致 */
需要注意的是,Subgrid 要求父元素的行轨道在子网格跨越的范围内是显式定义的(或者由 grid-auto-rows 生成)。同时它要求浏览器支持——主流现代浏览器已经全部支持,但如果你的项目还需要兼容较老的版本,需要准备降级方案(通常就是回退到手动重复列定义)。
十、经典布局模式合集
下面把前端历史上几个著名的布局难题,用 Grid 重新实现一遍。点击卡片可以切换查看对应的代码。
.holy-grail {
display: grid;
grid-template-columns: 200px minmax(0, 1fr) 200px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"left main right"
"footer footer footer";
gap: 16px;
min-height: 100vh;
}
/* 在窄屏上,两侧栏自动堆叠 */
@media (max-width: 768px) {
.holy-grail {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"left"
"right"
"footer";
}
}
双飞翼:中间内容优先加载
双飞翼布局的核心诉求是:DOM 里把主内容放在最前面,保证它在网络较慢时优先渲染;两侧栏在视觉上分列左右。用 Grid 的 grid-template-areas,DOM 顺序和视觉顺序可以完全解耦:
<main>主内容</main> <!-- DOM 里排第一 -->
<aside class="left">左侧</aside>
<aside class="right">右侧</aside>
</div>
.wing {
display: grid;
grid-template-columns: 180px minmax(0, 1fr) 180px;
grid-template-areas: "left main right";
}
.wing > .left { grid-area: left; }
.wing > main { grid-area: main; }
.wing > .right { grid-area: right; }
在 Flexbox 时代,双飞翼需要负边距、浮动、order 层层配合,才能既保证 DOM 顺序又保证视觉顺序。Grid 把这件事变成了一句 grid-template-areas。
十一、Grid 与 CSS 变量的化学反应
Grid 的轨道定义里可以引用 CSS 变量,这带来了一个极其优雅的能力:用一个变量控制整套布局。
--sidebar-width: 240px;
--gap: 20px;
--card-min: 260px;
}
.layout {
display: grid;
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
gap: var(--gap);
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(var(--card-min), 1fr));
gap: var(--gap);
}
/* 折叠状态:只改变量,不用碰任何布局属性 */
.layout.is-collapsed {
--sidebar-width: 64px;
}
这种写法的好处是状态与布局分离。折叠侧边栏、切换紧凑模式、调整卡片密度,都只需要改变量,不需要写新的 Grid 规则。如果你用 JS 动态控制,也只需要 element.style.setProperty('--sidebar-width', '64px') 一行。
用变量做“密度切换”
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--item-min, 200px), 1fr));
gap: var(--density-gap, 16px);
}
/* 紧凑模式 */
.list.is-compact {
--item-min: 140px;
--density-gap: 8px;
}
十二、Grid 的动画与过渡
Grid 的轨道尺寸是可以过渡的——这是它相对 Flexbox 的一个独特优势。你可以让列宽平滑变化,而不需要任何 JavaScript。
display: grid;
grid-template-columns: 0fr 1fr;
transition: grid-template-columns 0.3s ease;
}
.expandable.is-open {
grid-template-columns: 240px 1fr;
}
/* 关键:0fr 轨道里的内容需要 overflow: hidden 才能被收起 */
.expandable > .panel {
overflow: hidden;
}
注意 0fr 这个值——它是 Grid 独有的技巧。轨道宽度为 0 时,配合子元素的 overflow: hidden,就能实现“从无到有”的展开动画。这在做侧边抽屉、折叠面板时非常有用。
不要对 grid-template-areas 做动画
grid-template-areas 是离散值,无法插值过渡。想让区域位置变化时有动画,只能:
- 对具体的
grid-column/grid-row做过渡(它们也是可插值的整数,部分浏览器支持)。 - 用
transform移动元素本身,而不是移动网格位置。 - 使用 FLIP 技巧:先记录旧位置,改变布局,再记录新位置,然后用
transform从旧位置动画到新位置。
十三、性能、陷阱与可访问性
Grid 的性能到底如何
一个常见误解是“Grid 比 Flexbox 慢”。实际上,现代浏览器对两者的布局计算都做了大量优化,在绝大多数场景下差异可以忽略。真正影响性能的不是用哪个布局模式,而是这几件事:
| 影响因素 | 说明 | 建议 |
|---|---|---|
| 频繁改变轨道定义 | 修改 grid-template-* 会触发整个容器的重新布局 |
用类名切换,而不是逐帧修改 |
| 大量隐式轨道 | 几百个自动生成的行会显著增加布局计算量 | 用虚拟列表,只渲染可视区域 |
| 深层嵌套网格 | 每一层都要独立计算,且容易产生尺寸传递问题 | 嵌套不超过三层,优先用 Subgrid |
| 在 Grid 上做动画 | grid-template-columns 的过渡会触发逐帧重排 |
改 transform,或者用 FLIP 技巧 |
陷阱一:百分比与 gap 冲突
grid-template-columns: 33.33% 33.33% 33.33%;
gap: 20px;
/* ✅ 用 fr,间距自动扣除 */
grid-template-columns: repeat(3, 1fr);
gap: 20px;
陷阱二:grid-area 会隐式创建轨道
如果你写 grid-column: 5 / 7,但显式网格只有 3 列,浏览器会自动补出第 4、5、6 列。这些隐式轨道的高度由 grid-auto-columns 控制(默认是 auto)。很多时候布局“莫名其妙多出来一列”,原因就在这里。
grid-auto-columns: 1fr;
grid-auto-rows: minmax(120px, auto);
/* 排查:在 DevTools 里打开 Grid 覆盖层,一眼就能看到隐式轨道 */
陷阱三:display: contents 的副作用
display: contents 可以让一个元素的子元素“穿透”上来,成为外层网格的网格项。它看起来很方便,但有两个问题:
- 它会让元素本身的盒模型完全消失,背景、边框、内边距全部失效。
- 在部分浏览器与辅助技术组合下,它会导致元素从可访问性树中消失,影响屏幕阅读器。
如果只是为了“让孙子元素参与网格”,更稳妥的做法是调整 DOM 结构,或者把中间层也设成 display: grid 并用 subgrid。
陷阱四:min-width: auto 导致的溢出
网格项默认的 min-width 是 auto,这意味着它不会小于内容的固有宽度。当一个网格项里放了 overflow: auto 的表格或长代码块时,会出现“设置了 overflow 却不滚动,反而把网格撑破”的现象。解决办法:
grid-template-columns: minmax(0, 1fr);
/* 方案二:直接给网格项设 min-width */
.grid > * { min-width: 0; }
可访问性:视觉顺序不能违背 DOM 顺序
Grid 允许你把任意元素放到任意位置,这带来了一个隐患:视觉顺序与 DOM 顺序不一致。键盘用户按 Tab 键时,焦点仍然按照 DOM 顺序移动,于是会出现“焦点从页面底部跳到顶部”的混乱体验。
grid-template-areas 重新组织,并确保阅读顺序合理。
grid-row 把页脚强行提到顶部,或者用 order 把重要按钮排到前面——视觉上好看,键盘和屏幕阅读器用户会崩溃。
另外两点:不要用 Grid 做纯视觉的表格(应该用 <table>),不要用空 div 占位(应该用 grid-column: span n 或空单元格字符串)。
打印样式里的 Grid
Grid 在打印场景下表现良好,但要注意分页问题。一个跨越多行的大网格项可能会被切在两页之间:
.cards {
/* 打印时固定为两列,避免卡片被压得太窄 */
grid-template-columns: 1fr 1fr;
}
.card {
/* 尽量不拆分卡片 */
break-inside: avoid;
}
}
调试技巧:DevTools 的 Grid 面板
Chrome、Firefox、Edge 的开发者工具都内置了 Grid 覆盖层。在 Elements 面板里,带有 display: grid 的元素旁边会有一个小网格图标,点击它就能:
- 把网格线、轨道编号、命名线全部可视化叠加在页面上。
- 查看
grid-template-areas定义出的区域名称。 - 高亮显示隐式轨道(通常用不同颜色区分)。
- 实时修改轨道定义,观察布局变化。
这个工具能极大缩短调试时间。遇到“元素怎么跑到那里去了”的问题,打开覆盖层,答案通常一眼可见。
十四、实战一:后台管理系统骨架
下面是一个真实项目中常见的后台布局需求:顶部固定导航、左侧可折叠菜单、中间内容区、右侧信息面板、底部状态栏。用 Grid 写出来,核心代码不到 30 行。
.admin {
display: grid;
grid-template-columns: var(--sidebar-w, 240px) minmax(0, 1fr) 320px;
grid-template-rows: 56px minmax(0, 1fr) 32px;
grid-template-areas:
"topbar topbar topbar"
"sidebar content aside"
"status status status";
height: 100dvh;
overflow: hidden;
}
.admin__topbar { grid-area: topbar; }
.admin__sidebar { grid-area: sidebar; overflow-y: auto; }
.admin__content { grid-area: content; overflow-y: auto; }
.admin__aside { grid-area: aside; overflow-y: auto; }
.admin__status { grid-area: status; }
/* ---------- 折叠侧边栏:只改变一个变量 ---------- */
.admin.is-collapsed {
--sidebar-w: 64px;
}
/* ---------- 中屏:收起右侧面板 ---------- */
@media (max-width: 1280px) {
.admin {
grid-template-columns: var(--sidebar-w, 240px) minmax(0, 1fr);
grid-template-areas:
"topbar topbar"
"sidebar content"
"status status";
}
.admin__aside { display: none; }
}
/* ---------- 小屏:侧边栏变成抽屉 ---------- */
@media (max-width: 768px) {
.admin {
grid-template-columns: 1fr;
grid-template-areas:
"topbar"
"content"
"status";
}
.admin__sidebar {
position: fixed;
inset: 56px auto 32px 0;
width: 260px;
transform: translateX(-100%);
transition: transform .25s ease;
}
.admin.is-open .admin__sidebar {
transform: translateX(0);
}
}
这段代码里有几个值得注意的设计决策:
--sidebar-w,折叠时只改这个变量
overflow-y: auto,互不影响
grid-template-areas,DOM 完全不变
十五、实战二:杂志式内容网格
第二个实战是一个更偏“内容展示”的场景:一个首页的杂志式布局,包含通栏大图、两栏文章、三栏卡片。这类布局如果用 Flexbox 写,需要层层嵌套和大量计算;用 Grid 加命名线,可以写得非常优雅。
.magazine {
display: grid;
grid-template-columns:
[full-start] minmax(24px, 1fr)
[wide-start] minmax(0, 200px)
[main-start] minmax(0, 640px)
[main-end] minmax(0, 200px)
[wide-end] minmax(24px, 1fr)
[full-end];
gap: 32px 0;
}
/* ---------- 三种内容宽度,按名字使用 ---------- */
.magazine > * { grid-column: main; }
.magazine > .is-wide { grid-column: wide; }
.magazine > .is-full { grid-column: full; }
/* ---------- 三栏卡片:在 main 区域内再切分 ---------- */
.magazine__cards {
grid-column: wide;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 24px;
}
/* ---------- 首屏大图:整屏宽 + 文字叠加 ---------- */
.magazine__hero {
grid-column: full;
display: grid;
min-height: 420px;
place-items: center;
position: relative;
}
/* 图片与文字叠在同一格子里 */
.magazine__hero img {
grid-area: 1 / 1;
width: 100%;
height: 100%;
object-fit: cover;
}
.magazine__hero h1 {
grid-area: 1 / 1;
position: relative;
color: white;
font-size: clamp(1.8rem, 4vw, 3rem);
text-shadow: 0 2px 12px rgba(0,0,0,.6);
padding: 0 24px;
text-align: center;
}
这套写法最大的好处是语义化:HTML 里只需要给元素加 is-wide 或 is-full 这样的类名,具体宽度由栅格定义统一管理。想调整内容宽度?改 minmax() 里的数字就行,全站生效。
重叠布局:Grid 的独门绝技
上面 grid-area: 1 / 1 的用法值得单独说明。把多个元素放进同一个格子,它们就自然重叠了。这比传统的 position: absolute 方案好在:
- 不需要给父元素设
position: relative。 - 不需要计算 top/left/right/bottom。
- 重叠的元素仍然参与网格的对齐系统,可以用
place-items控制位置。 - 元素的尺寸仍受网格约束,不会脱离文档流导致父元素高度塌陷。
.avatar-wrap {
display: grid;
width: 64px;
height: 64px;
place-items: center;
}
.avatar-wrap > img {
grid-area: 1 / 1;
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-wrap > .badge {
grid-area: 1 / 1;
align-self: start;
justify-self: end;
transform: translate(20%, -20%);
width: 18px;
height: 18px;
border-radius: 50%;
background: #ef4444;
border: 2px solid white;
}
十六、面试高频问答
以下是 Grid 相关面试中反复出现的几个问题,点击展开答案。
Flexbox 是一维布局,一次只处理一个方向(行或列),适合内容驱动的排列,比如导航条、按钮组、卡片内部。Grid 是二维布局,行列同时定义,适合容器驱动的页面骨架、卡片列表、仪表盘。
选择法则:需要同时控制行和列,或者需要元素跨行跨列、重叠时,用 Grid;只需要沿一条轴线排队,或者希望容器跟着内容走时,用 Flexbox。真实项目里两者经常嵌套使用:外层 Grid 搭骨架,内层 Flexbox 排元素。
1fr 表示“剩余空间的一份”,它会无脑均分剩余空间,不关心内容尺寸。auto 表示“由内容决定,但如果有剩余空间也可以被拉伸”,它会先照顾内容的固有宽度。
举例:容器 600px,两列 1fr 1fr,结果各 300px;两列 auto auto,如果第一列内容宽 100px,第二列内容宽 200px,结果可能是第一列 200px、第二列 400px(按比例分配剩余空间),也可能各占内容宽度后剩余空间留白,具体取决于内容能否拉伸。
因为 1fr 等价于 minmax(auto, 1fr),最小值 auto 会取 min-content。当内容的最小宽度大于均分值,轨道就被撑大,整个网格溢出。
解决方法是显式写成 minmax(0, 1fr),把最小值设为 0,轨道就可以被压缩,超出的内容交给 overflow 处理。所有可能包含长文本、长 URL、表格的轨道都应该这样写。
auto-fill:尽可能多地创建轨道,即使没有内容也保留空轨道,项目靠左排列,右侧留白。
auto-fit:先创建轨道,然后把没有内容的空轨道折叠掉,剩余项目拉伸填满整行。
绝大多数卡片列表用 auto-fit;需要保持固定列宽、不想让卡片被拉宽时用 auto-fill。
不可以。每个命名区域必须是矩形,L 形、断开的名字会直接报错。如果需要不规则形状,只能拆成多个矩形区域分别命名,或者用 grid-column / grid-row 手动定位。
Subgrid 让嵌套的网格可以继承父网格的轨道。在此之前,子网格想对齐父网格的列,只能手动重复一遍尺寸定义,父网格改了子网格就错位。
最典型的应用是卡片列表里所有卡片的标题行、正文行、按钮行对齐到同一水平线。在没有 Subgrid 的年代需要固定高度或 JS 测量,现在交给浏览器即可。
现代浏览器对 Grid 的布局计算做了大量优化,正常使用下与 Flexbox 差异可以忽略。真正影响性能的是:频繁改变轨道定义(触发整个容器重排)、大量隐式轨道(几百行)、深层嵌套网格、在 Grid 上做逐帧动画。
建议:用类名切换而不是逐帧修改轨道;大量数据用虚拟列表;嵌套不超过三层;动画改 transform 而非 grid-template-*。
十七、速查表与落地清单
最后把全文浓缩成一份可以贴在工位上的速查表。
常用属性速查
| 需求 | 属性 | 常用值 |
|---|---|---|
| 开启网格 | display |
grid / inline-grid |
| 定义列 / 行 | grid-template-columns / -rows |
repeat(3, 1fr)、minmax() |
| 用命名区域描述布局 | grid-template-areas |
多行字符串 |
| 间距 | gap / row-gap / column-gap |
20px、1rem |
| 自动填充列 | repeat(auto-fit, minmax()) |
minmax(240px, 1fr) |
| 隐式轨道尺寸 | grid-auto-rows / -columns |
minmax(120px, auto) |
| 流动方向 | grid-auto-flow |
row / column / row dense |
| 项目定位 | grid-column / grid-row / grid-area |
2 / span 3、main |
| 容器内轨道对齐 | justify-content / align-content / place-content |
center、space-between |
| 项目在单元格内对齐 | justify-items / align-items / place-items |
center、stretch |
| 单个项目自我对齐 | justify-self / align-self |
end、start |
| 继承父级轨道 | grid-template-*: subgrid |
subgrid |
落地清单
- 先画草图,再写代码。在纸上把行列线画出来,比在浏览器里试要快十倍。
- 页面骨架用 Grid,组件内部用 Flexbox。不要用 Grid 去排一排按钮,也不要用 Flexbox 去搭整页布局。
- 所有可能溢出的轨道写成
minmax(0, 1fr)。这一条能避免 80% 的“莫名溢出”问题。 - 能用
fr就不要用百分比。fr会自动处理gap,百分比不会。 - 复杂布局优先用
grid-template-areas。可读性和可维护性远超数字定位。 - 响应式先试
auto-fit + minmax。很多“我以为需要媒体查询”的场景,它一行就够了。 - 媒体查询里只改
grid-template-*。组件本身的样式在断点之间应保持稳定。 - 视觉顺序必须与 DOM 顺序一致。Grid 给了你任意摆放的能力,但这不代表应该随便用。
- 不要给
grid-template-*加过渡动画。会触发逐帧重排,改用transform。 - 善用 DevTools 的 Grid 覆盖层。它会把网格线、轨道名、区域名全部可视化,排查问题快得多。
- 善用 CSS 变量。把侧边栏宽度、间距、卡片最小宽度抽成变量,一处修改全站生效。
- 嵌套超过三层就考虑 Subgrid。手动重复轨道定义是错位的根源。
/* 1. 页面骨架 —— 命名区域 */
.layout {
display: grid;
grid-template-columns: var(--sidebar, 240px) minmax(0, 1fr);
grid-template-areas: "side main";
min-height: 100dvh;
}
/* 2. 响应式卡片列表 —— 无需媒体查询 */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
}
/* 3. 内容栅格 —— 三种宽度语义 */
.content-grid {
display: grid;
grid-template-columns:
[full-start] 1fr [main-start] minmax(0, 720px) [main-end] 1fr [full-end];
}
/* 4. 完美居中 —— 三行搞定 */
.center {
display: grid;
place-items: center;
min-height: 100dvh;
}
/* 5. 重叠布局 —— 同一格子放多个元素 */
.overlay { display: grid; }
.overlay > * { grid-area: 1 / 1; }
/* 6. 自适应行高 —— 隐式行也有下限 */
.grid-rows {
grid-auto-rows: minmax(140px, auto);
}
/* 7. 0fr 展开动画 —— 无需 JS */
.expandable {
display: grid;
grid-template-columns: 0fr 1fr;
transition: grid-template-columns .3s ease;
}
.expandable.is-open {
grid-template-columns: 240px 1fr;
}
CSS Grid 最迷人的地方在于,它把布局从“用各种技巧拼凑”变成了“直接描述结构”。当你习惯了用网格线思考问题,会发现以前需要十几层嵌套才能实现的布局,现在只需要几行声明。
但它同样是一把双刃剑。Grid 给了你任意摆放元素的能力,也让“写出没人看得懂的布局”变得更容易。所以在享受强大能力的同时,请务必守住两条底线:命名要语义化,顺序要可预测。
最后一句话送给正在读到这里的人:好的布局代码,应该让人一眼就能在脑子里画出页面的样子。Grid 的命名线、命名区域、auto-fit,本质上都是为了让这句话成真。