CSS Grid布局的实用技巧

张玥 2026年9月20日 阅读时间 45分钟
CSS Grid布局 响应式 布局 最佳实践
前端开发技巧之CSS Grid布局的实用技巧

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,一个元素就够了:

/* Flexbox 版:三层嵌套 */
.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 的二维能力减少了对容器嵌套的依赖,DOM 结构更扁平
更强表达力 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 + 显式网格位置
学习曲线 较平缓,属性少 较陡,概念体系更完整

一个实用的选择法则

优先选 Grid
  • 你需要同时控制行和列
  • 布局结构是“先有框架,再往里填内容”
  • 需要让某些元素跨越多个单元格
  • 需要元素之间精确对齐到同一套基准线
  • 需要元素重叠(比如图片上叠文字)
优先选 Flexbox
  • 只有一行或一列,且不需要跨行跨列
  • 内容长度不定,希望容器跟着内容走
  • 需要 flex-grow 那种“按内容比例分配”的行为
  • 是组件内部的小结构,嵌套 Grid 反而增加认知负担

它们其实是最好的搭档

真实的页面往往是这样的:外层用 Grid 定义页面骨架,每个区域内部用 Flexbox 排列小元素,卡片列表用 Grid 做响应式,卡片内部又用 Flexbox 排列标题和按钮。

/* 外层:Grid 负责页面骨架 */
.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 的元素
网格项 容器的直接子元素。孙子元素不是网格项,除非它的父级也是网格容器
网格线 划分轨道的分界线。N 列有 N+1 条竖线,编号从 1 开始,也可以从 -1 倒数
网格轨道 两条相邻网格线之间的空间,也就是“一行”或“一列”
网格单元格 行与列交叉出的最小单位,是定位的基本格子
网格区域 由若干单元格组成的矩形区域,可以命名后直接用 grid-area 引用
显式网格 你用 grid-template-* 明确声明出来的行列
隐式网格 内容超出显式网格时,浏览器自动补出来的行列
间距 gap / row-gap / column-gap,只作用于轨道之间,不作用于容器边缘

一个必须记住的细节:只有直接子元素才是网格项

这是新手最容易踩的坑。下面的写法中,.item 是网格项,而 .inner 不是:

<div class="grid">
  <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
1 2 3 4 5 -5 -4 -3 -2 -1
/* 4 列的网格,共有 5 条竖线 */
/* 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”。用它做侧边栏非常合适:

.layout {
  display: grid;
  /* 侧边栏最多 280px,内容少时可以更窄 */
  grid-template-columns: fit-content(280px) 1fr;
  gap: 24px;
}

minmax():给轨道加上下限和上限

minmax(min, max) 是响应式 Grid 的基石。它定义了一条轨道的尺寸范围:

/* 侧边栏最小 200px,最大占 25%,中间弹性 */
.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 的分配结果。

1
2
3
4
5
6

四、fr 的分配算法:为什么它比百分比聪明

fr 看起来只是“等分”,但它的计算规则比想象中细致。理解这套规则,能帮你解释很多“为什么我的布局长这样”的疑惑。

三步分配法

  1. 先扣除固定项:把 px、auto、min-content 这类非弹性轨道按各自规则确定尺寸,同时扣除所有 gap。
  2. 剩下的才是“剩余空间”:这个剩余空间就是所有 fr 轨道要瓜分的总量。
  3. 按 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 去处理。

推荐做法
所有可能包含长文本、长 URL、表格、代码块的网格轨道,都写成 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 之后,浏览器会尝试用小项目把洞补上。

dense 的适用场景
图片瀑布流、标签云、卡片杂志式布局——这些场景中视觉紧凑比 DOM 顺序更重要。
dense 的代价
它会让视觉顺序与 DOM 顺序不一致,屏幕阅读器和键盘 Tab 顺序会变得难以预测。表单、导航这类强调顺序的场景不要用。

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 是三分之二宽,两列时它就是整行——布局逻辑保持一致。

一个完整的响应式卡片列表示例

.cards {
  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 是默认值——项目填满整个单元格。

justify-items: stretch | start | center | end; /* 水平 */
align-items: stretch | start | center | end; /* 垂直 */
place-items: center center; /* 简写 */

层级三:单个项目的自我对齐(*-self)

作用于具体的网格项,优先级高于容器上的 *-items:

.card--pinned {
  justify-self: end;
  align-self: start;
}

动手试试:切换对齐方式

点击下面的按钮,观察六个网格项在各自单元格里的位置变化。注意 stretch 会让项目铺满单元格,而其他值会让项目收缩到内容尺寸。

A
B
C
D
E
F
place-items: stretch

用 Grid 实现完美居中

这是一个被反复提及的场景,用 Grid 只需要三行:

.center-everything {
  display: grid;
  place-items: center;
  min-height: 100vh;
}

/* 等价于 Flexbox 的:
display: flex;
align-items: center;
justify-content: center; */

两者都可以,但 Grid 版本少写一行,而且当你有多个子元素需要同时居中时,Grid 能保证它们互相不干扰。

七、网格线定位与命名线

自动放置很方便,但真正的精确控制来自手动定位。你可以用编号,也可以用命名线——后者的可读性高出一大截。

用编号定位

.layout {
  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; }

这种写法的可读性不太好——你得在心里数线。而且在网格结构调整后,所有数字都要重新核对。

用命名线定位

在定义轨道时,用方括号给网格线起名字:

.layout {
  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 画图的方式描述整个页面结构。

header
sidebar
main
.page {
  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,卡片列表就能在任意宽度下自动决定列数:

.cards {
  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 的区别

这两个关键字只差一个字母,效果却明显不同。下面并排展示它们在同一容器、同一内容下的表现:

repeat(auto-fill, minmax(54px, 1fr))
1
2
3
repeat(auto-fit, minmax(54px, 1fr))
1
2
3
auto-fill 尽可能多地创建轨道,即使没有内容也保留空轨道。项目靠左排列,右侧留白
auto-fit 先创建轨道,然后把没有内容的空轨道折叠掉,剩余项目拉伸填满整行
选择建议 绝大多数卡片列表用 auto-fit;需要保持固定列宽、不想让卡片被拉宽时用 auto-fill

第二件:媒体查询,处理结构性变化

auto-fit 只能处理“列数变化”,无法处理“结构变化”(比如侧边栏从左边移到上面)。这类需求仍然要靠媒体查询:

.dashboard {
  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 还可以只继承行、不继承列,或者相反:

.card {
  grid-column: span 2;
  grid-row: span 3;
  display: grid;
  /* 行继承父级,列自己定义 */
  grid-template-rows: subgrid;
  grid-template-columns: repeat(2, 1fr);
}

/* 让卡片内部的头部/主体/底部自动对齐到同一行 */

最典型的应用是“卡片列表里,所有卡片的标题、正文、按钮都对齐到同一水平线”。在没有 Subgrid 的年代,这需要给每一部分固定高度,或者用 JavaScript 测量最大高度。有了 Subgrid,交给浏览器就好。

.card-grid {
  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 重新实现一遍。点击卡片可以切换查看对应的代码。

圣杯布局
header
left
main
right
footer
双飞翼
header
left
main
right
卡片流
仪表盘
/* 圣杯布局:两侧固定,中间自适应 */
.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 顺序和视觉顺序可以完全解耦:

<div class="wing">
  <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 变量,这带来了一个极其优雅的能力:用一个变量控制整套布局。

:root {
  --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') 一行。

用变量做“密度切换”

.list {
  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。

.expandable {
  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 冲突

/* ❌ 三列 33.33% + 两个 20px 间距 = 溢出 */
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 却不滚动,反而把网格撑破”的现象。解决办法:

/* 方案一:把轨道最小值设为 0 */
grid-template-columns: minmax(0, 1fr);

/* 方案二:直接给网格项设 min-width */
.grid > * { min-width: 0; }

可访问性:视觉顺序不能违背 DOM 顺序

Grid 允许你把任意元素放到任意位置,这带来了一个隐患:视觉顺序与 DOM 顺序不一致。键盘用户按 Tab 键时,焦点仍然按照 DOM 顺序移动,于是会出现“焦点从页面底部跳到顶部”的混乱体验。

推荐做法
保持 DOM 顺序与视觉顺序一致。需要移动位置时,优先调整 DOM 结构或使用 grid-template-areas 重新组织,并确保阅读顺序合理。
避免做法
用 grid-row 把页脚强行提到顶部,或者用 order 把重要按钮排到前面——视觉上好看,键盘和屏幕阅读器用户会崩溃。

另外两点:不要用 Grid 做纯视觉的表格(应该用 <table>),不要用空 div 占位(应该用 grid-column: span n 或空单元格字符串)。

打印样式里的 Grid

Grid 在打印场景下表现良好,但要注意分页问题。一个跨越多行的大网格项可能会被切在两页之间:

@media print {
  .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,折叠时只改这个变量
minmax(0, 1fr) 内容区允许被压缩,避免内部表格撑破布局
独立滚动 三个区域各自 overflow-y: auto,互不影响
100dvh + hidden 整体不滚动,滚动发生在区域内部,符合后台应用习惯
区域重排 不同断点只改 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 相关面试中反复出现的几个问题,点击展开答案。

1. Grid 和 Flexbox 的区别是什么?什么时候用哪个?

Flexbox 是一维布局,一次只处理一个方向(行或列),适合内容驱动的排列,比如导航条、按钮组、卡片内部。Grid 是二维布局,行列同时定义,适合容器驱动的页面骨架、卡片列表、仪表盘。

选择法则:需要同时控制行和列,或者需要元素跨行跨列、重叠时,用 Grid;只需要沿一条轴线排队,或者希望容器跟着内容走时,用 Flexbox。真实项目里两者经常嵌套使用:外层 Grid 搭骨架,内层 Flexbox 排元素。

2. 1fr 和 auto 有什么区别?

1fr 表示“剩余空间的一份”,它会无脑均分剩余空间,不关心内容尺寸。auto 表示“由内容决定,但如果有剩余空间也可以被拉伸”,它会先照顾内容的固有宽度。

举例:容器 600px,两列 1fr 1fr,结果各 300px;两列 auto auto,如果第一列内容宽 100px,第二列内容宽 200px,结果可能是第一列 200px、第二列 400px(按比例分配剩余空间),也可能各占内容宽度后剩余空间留白,具体取决于内容能否拉伸。

3. 为什么 1fr 有时候会溢出?怎么解决?

因为 1fr 等价于 minmax(auto, 1fr),最小值 auto 会取 min-content。当内容的最小宽度大于均分值,轨道就被撑大,整个网格溢出。

解决方法是显式写成 minmax(0, 1fr),把最小值设为 0,轨道就可以被压缩,超出的内容交给 overflow 处理。所有可能包含长文本、长 URL、表格的轨道都应该这样写。

4. auto-fill 和 auto-fit 有什么区别?

auto-fill:尽可能多地创建轨道,即使没有内容也保留空轨道,项目靠左排列,右侧留白。

auto-fit:先创建轨道,然后把没有内容的空轨道折叠掉,剩余项目拉伸填满整行。

绝大多数卡片列表用 auto-fit;需要保持固定列宽、不想让卡片被拉宽时用 auto-fill。

5. grid-template-areas 里的区域可以是不规则形状吗?

不可以。每个命名区域必须是矩形,L 形、断开的名字会直接报错。如果需要不规则形状,只能拆成多个矩形区域分别命名,或者用 grid-column / grid-row 手动定位。

6. Subgrid 解决了什么问题?

Subgrid 让嵌套的网格可以继承父网格的轨道。在此之前,子网格想对齐父网格的列,只能手动重复一遍尺寸定义,父网格改了子网格就错位。

最典型的应用是卡片列表里所有卡片的标题行、正文行、按钮行对齐到同一水平线。在没有 Subgrid 的年代需要固定高度或 JS 测量,现在交给浏览器即可。

7. Grid 布局会影响页面性能吗?

现代浏览器对 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。手动重复轨道定义是错位的根源。
// 一份可以常备的 Grid 工具箱骨架

/* 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,本质上都是为了让这句话成真。