如果只能推荐一个现代 CSS 布局工具,答案毫无悬念是 Grid。它把过去需要浮动、负边距、表格布局、JavaScript 计算才能勉强实现的版式,压缩成了几行声明式代码。display: grid 一出现,页面布局从“把盒子推到它该在的地方”变成了“先画一张地图,再让元素住进去”。然而真正把 Grid 用好的人并不多——大多数人只停留在 grid-template-columns: repeat(3, 1fr) 这一句上,遇到不规则排版、跨行跨列、响应式重排、圣杯布局、仪表盘,还是回到 Flexbox 甚至绝对定位。这篇 50 分钟的长文,会从心智模型讲到实战配方,配有六个可以当场动手的交互实验台,帮你把 Grid 从“会用”推到“用熟、用巧、用到别人看不出是 Grid 做的”。
一、为什么要学 Grid:一维已死,二维当立
在 Grid 之前,CSS 布局的历史大致经历了三个阶段:
- 表格布局时代:用
<table>排版,语义混乱,响应式几乎不可能。 - 浮动与定位时代:
float+clear+ 负 margin,需要清除浮动、需要计算宽度、需要“最后一列去 margin”这种补丁。 - Flexbox 时代:解决了单行或单列的弹性分配,但依然是一维的——它只能沿主轴排列,交叉轴上的对齐能力有限。
Grid 的定位非常明确:它是一个二维布局系统。你同时定义行和列,然后把元素放进由行列交叉形成的单元格里。Flexbox 关心的是“这一排元素怎么分配空间”,Grid 关心的是“这块区域如何被切分成网格”。两者不是替代关系,而是分工关系。
.cards {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.card {
flex: 1 1 calc((100% - 32px) / 3);
/* 还得担心最后一行只有 1~2 个卡片时被拉宽 */
}
/* 用 Grid 做同样的 3 列卡片 */
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
/* 最后一行不足 3 个时,卡片依然保持正确宽度 */
Grid 带来的三个观念转变
- 从“元素驱动”到“容器驱动”:过去先写元素再想办法排列,现在先在容器上定义网格,元素只是填进去而已。
- 从“计算宽度”到“声明比例”:再也不需要
calc((100% - 32px) / 3),一句1fr就够。 - 从“结构决定视觉”到“视觉与结构解耦”:用
grid-template-areas可以让视觉顺序与 DOM 顺序分离,这在响应式重排时威力巨大。
二、拆解 Grid:五个必须记住的名词
Grid 的术语不多,但每一个都必须精确理解,否则后面所有的定位语法都会变成“背咒语”。
/* 声明 display: grid 的元素,它定义了整个网格 */
.container { display: grid; }
/* 2. 网格项目 Grid Item */
/* 容器的直接子元素,自动成为网格项目 */
/* 注意:只有直接子元素,孙元素不是 */
/* 3. 网格线 Grid Line */
/* 划分网格的线,从 1 开始编号,
3 列会有 4 条列线,2 行会有 3 条行线 */
/* 4. 网格轨道 Grid Track */
/* 两条相邻网格线之间的空间,即一行或一列 */
/* 5. 网格区域 Grid Area */
/* 由 4 条网格线围成的矩形区域,
可以跨多行多列 */
/* 一张图记住编号规则:*/
/* 列线 1 列线 2 列线 3 列线 4 */
/* | 列1 | 列2 | 列3 | */
/* 行线1 +-------+-------+-------+ */
/* | | | | */
/* 行线2 +-------+-------+-------+ */
/* | | | | */
/* 行线3 +-------+-------+-------+ */
3 列 × 2 行 = 4 条列线 + 3 条行线
网格线编号可以倒数
这是 Grid 里最容易被忽略、却极其好用的特性:网格线可以从末尾倒数,用负数表示。-1 永远表示最后一条线,-2 表示倒数第二条。这意味着你不需要知道网格到底有几列,就能让元素横跨到最右边。
.full-bleed {
grid-column: 1 / -1;
}
/* 让元素占据最后两列 */
.last-two {
grid-column: -3 / -1;
}
/* 从第 1 条行线一直延伸到最底部 */
.full-height {
grid-row: 1 / -1;
}
三、从零开始:一个最小可用的 Grid
先把最简单的情况吃透。下面这段代码创建了一个 3 列 2 行的网格,六个子元素自动填充进去。
<div class="grid">
<div>1</div> ... 共 6 个
</div>
/* CSS:最小可用配置 */
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 80px 80px;
gap: 16px;
}
/* 等价写法:用 repeat() 更简洁 */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 80px);
gap: 16px;
}
只有 display: grid 就够了
如果你只写 display: grid,什么都不会发生——所有子元素会挤成一列,每个元素占据一行。这是因为浏览器默认给了一个单列网格。你可以把它理解成:Grid 的默认行为是“垂直堆叠”,这恰好和块级元素的表现一致。
所以真正让 Grid 产生威力的是 grid-template-columns 和 grid-template-rows。只写列、不写行是最常见的写法——行会由内容自动撑开,这就是所谓的隐式行,我们后面会详细讲。
四、fr 单位:弹性布局的数学核心
fr 是 Grid 引入的新单位,全称 fraction(份)。它的语义非常直白:把剩余空间按份数分配。三个 1fr 就是三等分;1fr 2fr 1fr 就是 1:2:1 分配。
关键在于“剩余”两个字。如果网格里还有固定宽度的轨道或 auto 轨道,浏览器会先把它们算完,剩下的空间才交给 fr 去分。理解这一点,就能解释很多“为什么我的 fr 不按比例”的困惑。
每个方块下方显示的是它实际计算出的像素宽度。拖动滑块,观察三列宽度如何按比例变化。
fr 与 auto 的区别
很多人会混淆 1fr 和 auto,它们的行为完全不同:
/* 内容宽的列会更宽,内容窄的列会更窄 */
.auto-cols {
grid-template-columns: auto auto auto;
}
/* 1fr:忽略内容尺寸,强制等分剩余空间 */
/* 内容再长也只会撑破格子,不会改变列宽比例 */
.fr-cols {
grid-template-columns: 1fr 1fr 1fr;
}
/* 重要:1fr 的隐含最小尺寸是 auto */
/* 也就是说,1fr 的列最小不会小于内容宽度 */
/* 结果:长内容会把列撑开,破坏等分效果 */
/* ✅ 修复:用 minmax(0, 1fr) 解除最小尺寸限制 */
.safe-cols {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
/* 这样即使内容里有长英文、长链接、pre 标签 */
/* 列宽依然严格等分,超出部分由 overflow 处理 */
grid-template-columns: 1fr 1fr 1fr;第三列里放了一个超长的 URL,结果第三列被撑得特别宽,前两列被挤压。
grid-template-columns: repeat(3, minmax(0, 1fr));列宽严格等分,长内容在格子内部滚动或截断。
记住这个心法
1fr 不是“等分”,而是“分一份”;minmax(0, 1fr) 才是“严格等分”。只要你在项目里遇到过“网格被长内容撑爆”的问题,八成就是漏了 minmax(0, 1fr)。这是 Grid 实战中最实用的一条经验。
五、repeat() 与 minmax():把重复代码压成一行
Grid 提供了一整套“简写工具”,让复杂的网格定义变得短小精悍。掌握它们,代码量能减少一半以上。
grid-template-columns: 1fr 1fr 1fr 1fr;
/* 等价于 */
grid-template-columns: repeat(4, 1fr);
/* repeat() 可以混合固定值与弹性值 */
grid-template-columns: 200px repeat(3, 1fr) 200px;
/* minmax():定义轨道的最小与最大尺寸 */
grid-template-columns: repeat(3, minmax(200px, 1fr));
/* 每列最小 200px,最大可以分到 1fr 的空间 */
/* minmax() + auto:高度自适应但不超过内容 */
grid-template-rows: minmax(120px, auto);
/* 固定值与弹性值混用 */
grid-template-columns: 240px minmax(0, 1fr) 240px;
/* 典型的侧边栏 + 主内容 + 侧边栏三栏布局 */
/* 常用组合:自动填充 + 最小宽度 + 弹性最大宽度 */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
/* 这一行几乎能解决 80% 的响应式卡片列表需求 */
宽度恒定,容器再宽也不变
按份数瓜分剩余空间
既弹性又有上下限,最实用
由内容决定尺寸
另一个冷门但好用的值:fit-content()
fit-content(300px) 的语义是:“内容需要多宽就多宽,但最多不超过 300px”。它和 minmax(auto, 300px) 类似,但在实际使用中更直观。适合用在标签栏、日期列、序号列这类“通常很窄,但偶尔会变宽”的场景。
.table-like {
display: grid;
grid-template-columns: fit-content(120px) 1fr fit-content(100px);
gap: 12px;
}
六、auto-fill 与 auto-fit:响应式卡片网格的真相
这一节的内容,是 Grid 在日常开发中出场率最高的场景:让卡片列表在不同屏幕宽度下自动调整列数,且不需要写任何媒体查询。
当前轨道数:--
两者的唯一区别
auto-fill 和 auto-fit 在计算轨道数量时行为完全一致,都是“能塞几列就塞几列”。区别只在最后一步:
- auto-fill:如果容器还有剩余空间,创建空轨道占位。剩下的空间就空在那里。
- auto-fit:把没有内容的空轨道折叠掉,让已有的轨道拉伸填满整行。
换句话说:当卡片数量足够多、能填满整行时,两者看起来完全一样;只有当最后一行的卡片数量不足以填满时,差异才会显现——auto-fill 会让卡片保持原宽度、右侧留白,auto-fit 会让卡片被拉宽、铺满整行。
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
/* 这行代码的含义,展开来说是:*/
/* 1. 每一列最小 280px,最大不超过 1fr */
/* 2. 浏览器计算容器能放下几列 */
/* 3. 空隙被已有的列自动吸收 */
/* 4. 全程不需要媒体查询 */
/* 对比:用媒体查询的旧方案 */
.card-grid-old {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
@media (min-width: 640px) {
.card-grid-old { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 960px) {
.card-grid-old { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1280px) {
.card-grid-old { grid-template-columns: repeat(4, 1fr); }
}
容器变宽时列数自动增加,容器变窄时自动减少
该选 auto-fit 还是 auto-fill?
- 选 auto-fit:卡片数量不固定、希望铺满整行、不希望出现右侧突兀留白。这是大多数卡片列表的选择。
- 选 auto-fill:希望卡片尺寸保持稳定、不希望最后几张被拉得过宽。例如图标网格、缩略图墙、仪表盘上的小部件。
一个实用的判断技巧:如果你希望所有卡片宽度永远一致,用 auto-fill;如果你希望整行永远被填满,用 auto-fit。
七、gap 与对齐:把间距交给 Grid,把 margin 扔掉
在 Flexbox 和 Grid 出现之前,元素之间的间距几乎全靠 margin。而 margin 的副作用是:它会参与布局计算,会引发外边距折叠,还会让你不得不写 :last-child { margin-right: 0 } 这种补丁。
gap 彻底解决了这个问题:它只存在于轨道之间,不参与元素尺寸计算,不会折叠,也不需要处理首尾元素。
网格轨道是固定尺寸(88px × 62px),所以当 justify-items 不是 stretch 时,项目会缩到内容大小,对齐效果一目了然。
gap 的三种写法
.grid { gap: 16px; }
/* 两个值:行间距 列间距 */
.grid { gap: 16px 24px; }
/* 分开写 */
.grid {
row-gap: 16px;
column-gap: 24px;
}
/* 旧写法 grid-gap 已被 gap 取代,不再推荐 */
对齐属性的完整体系
Grid 的对齐属性很多,但它们有清晰的命名规则:前半个词决定方向,后半个词决定作用对象。
| 属性 | 作用对象 | 常用值 |
|---|---|---|
justify-items |
所有项目在行内方向(水平)上的对齐 | stretch / start / center / end |
align-items |
所有项目在块方向(垂直)上的对齐 | stretch / start / center / end |
justify-self |
单个项目水平对齐(写在项目上) | stretch / start / center / end |
align-self |
单个项目垂直对齐(写在项目上) | stretch / start / center / end |
justify-content |
整个网格内容在容器中水平分布 | start / center / space-between / space-around |
align-content |
整个网格内容在容器中垂直分布 | start / center / space-between / space-around |
place-items |
align-items + justify-items 简写 |
center / start / end |
place-content |
align-content + justify-content 简写 |
center / start / space-between |
记忆口诀:items 管项目,content 管整体,self 管自己;justify 是水平,align 是垂直。place-* 是前两者的简写,顺序是 align 在前、justify 在后。
一个高频需求:让某个项目单独居中
.grid {
display: grid;
place-items: center;
}
/* 只有某一个项目居中 */
.grid .special {
justify-self: center;
align-self: end;
}
/* 经典:一个 div 里的元素水平垂直居中 */
.center-box {
display: grid;
place-items: center;
min-height: 200px;
}
八、网格线定位:从“自动排列”到“精确落位”
前面所有例子里,元素都是自动填充的。而 Grid 真正强大的地方在于:你可以精确指定每个元素占据哪几条网格线。点击下面的任意单元格,看看它对应的定位代码。
这是一个 4 列 × 3 行的网格。每个单元格都可以用 grid-column 与 grid-row 精确描述。
四种定位写法
.item {
grid-column: 2 / 4;
grid-row: 1 / 3;
}
/* 占据第 2~3 列、第 1~2 行 */
/* 写法 2:起始线 + span 跨越数 */
.item {
grid-column: 2 / span 2;
grid-row: 1 / span 2;
}
/* 效果与写法 1 完全相同,但更易读 */
/* 写法 3:只写起始线 */
.item {
grid-column-start: 2;
grid-column-end: 4;
}
/* 写法 4:负号从末尾倒数 */
.item {
grid-column: 1 / -1;
}
/* 横跨整个网格,无论多少列 */
grid-column: 1 / 3 或 1 / span 2
grid-row: 2 / 4 或 2 / span 2
grid-column: 1 / -1 最省心
span 的两种写法差异
grid-column: 2 / span 2 与 grid-column: span 2 / 4 都能实现“跨两列”,但语义不同:
2 / span 2:从第 2 条线开始,往后跨 2 条。起点固定,终点由跨数决定。span 2 / 4:在第 4 条线结束,往前跨 2 条。终点固定,起点由跨数决定。
在自动布局的场景里,后者往往更稳妥,因为它保证了元素右侧的边界固定,不会因为前面元素的位置变化而整体漂移。
一个立竿见影的例子:图片网格
.gallery {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 120px;
gap: 12px;
}
.gallery > :first-child {
grid-column: span 2;
grid-row: span 2;
}
/* 其余图片自动填充到剩下的格子里 */
九、grid-area 与命名区域:把布局画成一张地图
如果只能选一个 Grid 特性来展示它的“爽感”,我会选 grid-template-areas。它允许你用可视化的字符串直接描述页面结构,一眼就能看懂布局长什么样。
同一套 HTML,只换一份 grid-template-areas,就能得到完全不同的版式。
语法规则
同一行内用空格分隔每一列 */
.layout {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
}
/* 元素通过 grid-area 认领自己的区域 */
.layout > header { grid-area: header; }
.layout > .sidebar { grid-area: sidebar; }
.layout > main { grid-area: main; }
.layout > aside { grid-area: aside; }
.layout > footer { grid-area: footer; }
/* 用点号表示空白区域 */
.layout-dots {
grid-template-areas:
"header header ."
"sidebar main main";
}
/* 连续的点号可以简写成一个 */
/* "..." 与 ". . ." 等价 */
响应式重排:一行媒体查询搞定
这是 grid-template-areas 最有价值的用法。在移动端把三栏改成单栏,只需要重写 areas 和 columns,不需要动任何 DOM。
.layout {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
}
/* 移动端:单栏,侧边栏挪到主内容下方 */
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"aside"
"sidebar"
"footer";
}
}
注意这里发生了什么:视觉顺序变了,但 DOM 顺序完全没变。屏幕阅读器依然按照 header → sidebar → main → aside → footer 的顺序朗读,搜索引擎也依然按原结构解析。这就是 Grid 在无障碍方面胜过“用绝对定位强行重排”的根本原因。
十、隐式网格与 grid-auto-flow:当元素溢出了
当你只定义了列、没有定义行时,元素会自动创建新行——这些新行叫做隐式行。它们的高度由 grid-auto-rows 决定,默认为 auto(由内容撑开)。
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
/* 控制隐式行的高度 */
.grid {
grid-auto-rows: 120px;
}
/* 或使用 minmax 让行高有弹性 */
.grid {
grid-auto-rows: minmax(100px, auto);
}
/* 控制隐式列的宽度 */
.grid {
grid-auto-columns: 1fr;
}
/* grid-auto-flow:控制自动填充的方向 */
grid-auto-flow: row; /* 默认,逐行填充 */
grid-auto-flow: column; /* 逐列填充 */
grid-auto-flow: row dense; /* 逐行填充 + 填补空洞 */
grid-auto-flow: column dense;
dense 的典型场景
想象一个图片墙,大部分图片占 1×1,少数图片占 2×2。普通模式下,大图后面的位置会留空;开启 dense 后,小图会自动回填到空洞里,整个网格变得紧凑。
.masonry-like {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-auto-rows: 100px;
grid-auto-flow: row dense;
gap: 10px;
}
.masonry-like .wide { grid-column: span 2; }
.masonry-like .tall { grid-row: span 2; }
.masonry-like .big { grid-column: span 2; grid-row: span 2; }
/* 重要提醒:dense 会改变视觉顺序 */
/* 如果内容有阅读顺序要求,请谨慎使用 */
十一、重叠与层叠:Grid 天生支持 Z 轴
Grid 有一个其他布局方式都做不到的能力:多个元素可以占据同一个网格单元格。它们会互相重叠,通过 z-index 控制前后顺序。这在做图文叠加、封面标题、图片说明时非常方便。
.hero {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.hero > * {
grid-area: 1 / 1 / 2 / 2; /* 全部重叠 */
}
.hero .text {
z-index: 1;
align-self: end;
padding: 24px;
color: white;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
}
/* 图片铺满容器 */
.hero img {
width: 100%;
height: 100%;
object-fit: cover;
}
A 与 B 重叠,B 通过 z-index 叠在 A 上方
经典应用:卡片上的浮动标签
.product {
display: grid;
grid-template-areas: "cover";
border-radius: 12px;
overflow: hidden;
}
.product > img { grid-area: cover; }
.product > .badge {
grid-area: cover;
justify-self: end;
align-self: start;
margin: 12px;
padding: 4px 10px;
background: #ef4444;
color: white;
border-radius: 10px;
font-size: 0.72rem;
}
这套写法的优雅之处在于:不需要 position: absolute,不需要给父元素加 position: relative,角标自动叠在图片上,还能用 justify-self / align-self 精确控制它出现在四个角落中的哪一个。
十二、subgrid:让子网格继承父网格轨道
你有没有遇到过这种情况:一个卡片列表,每张卡片内部有自己的标题、正文、按钮。你希望所有卡片的标题在垂直方向上严格对齐,但每张卡片的高度由内容决定,很难做到。
subgrid 就是为了解决这类问题而生的。它允许子元素直接使用父网格的轨道,从而实现跨卡片的对齐。
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
/* 子网格:卡片内部的行由父级统一定义 */
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
/* 结果:所有卡片的标题行、正文行、按钮行
在垂直方向上严格对齐,无论内容多长 */
/* 列方向也可以继承 */
.form-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
/* 注意:subgrid 需要元素本身是网格项目 */
/* 也就是说,必须先有 display: grid */
没有 subgrid 时的替代方案
在 subgrid 普及之前,社区常用两种“土办法”实现卡片内对齐:
- 固定行高:给卡片的标题、正文、按钮分别设置固定高度。缺点是内容长度不可控,容易溢出或留白。
- Flexbox + margin-top: auto:把卡片设为纵向 Flex,按钮用
margin-top: auto推到底部。这能解决“按钮贴底”,但解决不了标题跨卡片对齐。
所以如果目标用户主要使用现代浏览器,subgrid 是更彻底的方案。使用前建议先确认项目的浏览器支持范围。
十三、实战一:响应式卡片网格
把前面的知识点串起来,做一个最常用、也最容易出错的东西:卡片列表。
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: 24px;
}
/* 注意 min(280px, 100%) 这个技巧:*/
/* 当容器宽度小于 280px 时,最小值退化为 100% */
/* 避免小屏出现横向溢出 */
/* 进阶版:控制每行最多 4 列 */
.card-grid-limited {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
max-width: 1280px;
margin-inline: auto;
gap: 24px;
}
/* 卡片内部:纵向 Flex,按钮贴底 */
.card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
background: white;
border-radius: 12px;
contain: layout paint;
}
.card .actions {
margin-top: auto;
}
/* 长文本防溢出 */
.card h3 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
三个必须注意的细节
- 用
min(280px, 100%)而不是直接写 280px:否则在窄于 280px 的容器里,minmax 的最小值超过容器宽度,会导致横向滚动条。 - 卡片内部用 Flexbox:Grid 负责排列卡片,Flexbox 负责卡片内部。不要让一个 Grid 承担所有事情。
- 加
contain: layout paint:卡片数量多时,它能显著减少重绘范围,滚动更顺滑。
十四、实战二:整页骨架与圣杯布局
圣杯布局(Holy Grail Layout)是前端面试的经典题,也是 Grid 最能展示威力的场景。用 Flexbox 实现需要 order、负 margin、calc() 一堆技巧;用 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"
"nav main aside"
"footer footer footer";
min-height: 100vh;
gap: 0;
}
/* 关键:1fr 行让主内容区撑满剩余高度 */
/* 页脚永远贴在视口底部,无需任何 JS */
/* 各区域认领位置 */
.holy-grail > header { grid-area: header; }
.holy-grail > nav { grid-area: nav; }
.holy-grail > main { grid-area: main; }
.holy-grail > aside { grid-area: aside; }
.holy-grail > footer { grid-area: footer; }
/* 移动端重排:单栏堆叠 */
@media (max-width: 768px) {
.holy-grail {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"nav"
"aside"
"footer";
}
}
grid-template-rows: auto 1fr auto 让主内容自动撑满高度
为什么用 minmax(0, 1fr) 而不是 1fr
在中间主内容列上,这个细节至关重要。主内容区经常包含 <pre>、长表格、代码块,如果写 1fr,它会因为“1fr 的最小值是 auto”而被撑开,进而把整个页面撑出横向滚动条。写成 minmax(0, 1fr) 后,主内容列严格遵循 1fr 的分配,超出的内容由内部的 overflow: auto 处理。
body {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
margin: 0;
}
/* 主内容区允许内部滚动 */
main {
overflow: auto;
min-width: 0;
}
十五、实战三:后台仪表盘
后台管理系统是最能体现 Grid 价值的场景之一:顶部导航、侧边菜单、多个数据卡片、图表区、表格区,模块多且尺寸不一。用 grid-template-areas 描述,整个页面结构一目了然。
.dashboard {
display: grid;
grid-template-columns: 240px repeat(4, minmax(0, 1fr));
grid-template-rows: 64px auto auto minmax(0, 1fr);
grid-template-areas:
"topbar topbar topbar topbar topbar"
"side kpi1 kpi2 kpi3 kpi4"
"side chart chart chart list"
"side table table table list";
gap: 16px;
padding: 16px;
min-height: 100vh;
}
/* 侧边栏跨越 3 行,天然实现“固定侧边 + 滚动主区” */
/* 无需 position: fixed,也就不会有内容被遮挡的问题 */
仪表盘的响应式退化策略
后台系统通常以桌面为主,但也要考虑小屏。推荐的做法是分档处理:
- ≥ 1200px:完整五列布局,侧边栏常驻。
- 768px ~ 1200px:侧边栏收成图标条(80px),KPI 卡片从 4 列变 2 列。
- < 768px:侧边栏变成抽屉或底部 Tab,所有内容单列堆叠。
.dashboard {
grid-template-columns: 80px repeat(2, minmax(0, 1fr));
grid-template-areas:
"topbar topbar topbar"
"side kpi1 kpi2"
"side kpi3 kpi4"
"side chart chart"
"side table table";
}
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
grid-template-areas:
"topbar"
"kpi1"
"kpi2"
"kpi3"
"kpi4"
"chart"
"table"
"side";
}
}
十六、实战四:杂志式不规则排版
杂志风格排版的特点是:元素大小不一、跨越行列、错落有致。这种版式如果用绝对定位实现,几乎无法响应式;而用 Grid,只要定义好网格骨架,剩下的就是让元素“认领”不同大小的区域。
.magazine {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-auto-rows: minmax(120px, auto);
gap: 20px;
}
/* 头条:横跨全部 6 列 */
.magazine .hero {
grid-column: 1 / -1;
grid-row: span 2;
}
/* 主特稿:左侧 4 列,纵跨 3 行 */
.magazine .feature {
grid-column: 1 / 5;
grid-row: span 3;
}
/* 右侧边栏:占 2 列,纵跨 3 行 */
.magazine .side {
grid-column: 5 / 7;
grid-row: span 3;
}
/* 底部三张小编排:自动分配 */
.magazine .mini {
grid-column: span 2;
}
/* 移动端:全部单列 */
@media (max-width: 768px) {
.magazine {
grid-template-columns: 1fr;
}
.magazine > * {
grid-column: 1 / -1 !important;
grid-row: auto !important;
}
}
同一套网格骨架,通过不同的 span 组合出丰富版式
一个实用技巧:用 grid-auto-rows 控制节奏
杂志排版里,行高的选择至关重要。如果写 grid-auto-rows: 1fr,所有行会平均分配高度,内容少的行会显得空旷;如果写 auto,行高完全由内容决定,跨行元素的高度就难以预期。
最稳妥的做法是 grid-auto-rows: minmax(120px, auto):给一个最小高度保证节奏感,同时允许内容撑开。这样即使某个区块的内容特别多,也不会把整个版式挤垮。
十七、实战五:表单与标签对齐
表单是最容易被忽视的 Grid 应用场景。用 Grid 做表单,可以天然实现标签与输入框的对齐,并且支持“某些字段占满整行”。
.form {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
gap: 16px 20px;
align-items: center;
}
/* 标签右对齐,与输入框贴合 */
.form > label {
text-align: right;
color: #64748b;
}
/* 需要占满整行的字段 */
.form > .full {
grid-column: 1 / -1;
}
/* 让输入框内部对齐(多行文本) */
.form > textarea {
align-self: start;
}
/* 小屏:标签改为顶部 */
@media (max-width: 600px) {
.form {
grid-template-columns: 1fr;
gap: 6px;
}
.form > label {
text-align: left;
}
.form > .full {
grid-column: auto;
}
}
为什么不用 display: table 或 Flexbox
- 相比表格:Grid 不改变语义,不会引入无意义的 table 结构,且支持响应式重排。
- 相比 Flexbox:Flexbox 做不到“左侧标签列宽统一”,因为每个表单项都是独立的 Flex 容器,标签宽度各不相同。Grid 的两列模板天然保证了列宽一致。
这是一个非常典型的“二维问题用 Grid”的例子:表单的对齐需要同时考虑行和列,Flexbox 只能逐行处理。
十八、实战六:图片画廊与瀑布流近似方案
真正的瀑布流(Masonry)需要等图片加载完才知道高度,纯 CSS 一直做不到。不过用 Grid 的 dense 模式配合不同的 span,可以做出视觉上非常接近瀑布流的效果。
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-auto-rows: 10px; /* 细粒度行高 */
grid-auto-flow: dense;
gap: 12px;
}
/* 每张图根据自己的宽高比占不同的行数 */
.gallery img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
/* JS 根据图片比例设置 span */
const rowUnit = 10;
const gap = 12;
img.onload = () => {
const rows = Math.ceil((img.height / img.width) * 180 / (rowUnit + gap));
img.parentElement.style.gridRowEnd = `span ${rows}`;
};
/* 更精确的做法:CSS 多列布局 */
.masonry-columns {
columns: 3 240px;
column-gap: 16px;
}
.masonry-columns > * {
break-inside: avoid;
margin-bottom: 16px;
}
该选哪种方案?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Grid + dense | 纯 CSS,元素顺序可预测,性能好 | 需要 JS 计算 span,非真正瀑布流 |
| CSS columns | 真正的瀑布流效果,实现极简 | 阅读顺序是纵向的,不利于内容列表 |
| JS 绝对定位 | 完全可控 | 性能差、需要监听容器尺寸变化、维护成本高 |
grid-template-rows: masonry |
真正的原生瀑布流 | 规范仍在推进,浏览器支持有限 |
十九、Grid 与 Flexbox:到底该用谁
这是一个被问烂了的问题,但答案其实很清晰:看你的布局是几维的。
/* 一维排列,元素宽度由内容决定 */
.nav {
display: flex;
align-items: center;
gap: 24px;
}
/* 场景 2:卡片网格 —— 用 Grid */
/* 二维排列,需要行与列同时对齐 */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* 场景 3:卡片内部 —— 又用回 Flexbox */
/* 纵向排列,按钮贴底 */
.card {
display: flex;
flex-direction: column;
}
.card .btn { margin-top: auto; }
/* 结论:Grid 搭骨架,Flexbox 做组件 */
一个反直觉的事实
很多人以为 Grid 一定比 Flexbox “重”。实际上,对于一个简单的单行等分布局,两者的渲染性能几乎没有差别。真正影响性能的是你动画了哪些属性(见上一篇《高性能 CSS 动画技巧》),而不是你用了哪种布局方式。
所以选择标准应该回归到可读性与可维护性:哪种写法更能表达你的布局意图,就用哪种。
二十、常见陷阱清单
下面这些坑,几乎每个用 Grid 的人都会踩一遍。提前知道,可以省下大量调试时间。
grid-template-columns: 1fr 1fr;列里有长 URL,列宽被撑开。改为
minmax(0, 1fr)。
1fr 的隐含最小值是 auto,不会小于内容的 min-content 宽度。这是规范定义的,不是 bug。
"header header""main side extra"两行列数不同,整条声明会完全失效。
. 占位补齐,而不是省略。
<div class="grid"><div><span>...</span></div></div>span 不是网格项目,grid-column 对它无效。
display: contents。
grid-template-columns: repeat(3, 33.33%); gap: 20px;三列总宽 99.99%,再加上两个 gap,直接横向溢出。
1fr 而不是百分比。浏览器会自动把 gap 从可用空间里扣除,再分配 fr。
grid-auto-flow: dense,导致视觉顺序与 DOM 顺序不一致,屏幕阅读器与键盘 Tab 顺序混乱。
row。
.item { grid-template-columns: 1fr 1fr; }如果
.item 没有 display: grid,这句完全无效。
grid-* 系列属性只对网格容器生效。grid-column / grid-row 写在项目上,其余写在容器上。
grid-template-columns: repeat(3, 1fr); 写死三列,在窄屏上每列只剩 80px。
repeat(auto-fit, minmax(min(280px, 100%), 1fr)),让列数随容器宽度自动变化。
min-width: auto,长内容会撑破单元格。
min-width: 0,或使用 overflow: hidden / text-overflow: ellipsis。
二十一、无障碍与语义化
Grid 有一个容易被忽略的巨大优势:它允许视觉顺序与 DOM 顺序解耦,同时不破坏语义。这对无障碍来说意义重大。
<main class="layout">
<header>...</header>
<main>...</main> /* 主内容在前 */
<aside>...</aside>
<footer>...</footer>
</main>
/* 通过 areas 让 aside 在视觉上出现在右侧 */
.layout {
display: grid;
grid-template-columns: 1fr 300px;
grid-template-areas:
"header header"
"main aside"
"footer footer";
}
/* ❌ 反例:为了视觉效果调换 DOM 顺序 */
/* <aside> 写在 <main> 前面 */
/* 屏幕阅读器会先朗读侧边栏,用户要跳过一堆链接才能读到正文 */
dense 会破坏视觉顺序,内容型列表慎用
几个具体的无障碍建议
- 优先使用语义标签:
header、nav、main、aside、footer。Grid 不影响它们的语义,反而让结构更清晰。 - 不要用
order或密集流打乱焦点顺序:键盘用户的 Tab 顺序必须与视觉顺序基本一致。 - 给网格容器加合适的 role:如果网格是数据表格性质,考虑用真正的
<table>;如果是纯布局,不要加多余的 ARIA 角色。 - 尊重
prefers-reduced-motion:如果网格布局切换时带有动画过渡,需要在减少动效模式下关闭。
二十二、兼容性与渐进增强
Grid 的兼容性在 2026 年已经非常好了。核心特性几乎全平台可用,只有少数新特性需要留意。
.card-grid {
/* 降级:单列堆叠,所有浏览器都能看 */
display: block;
}
.card-grid > * {
margin-bottom: 20px;
}
/* 支持 Grid 时启用网格布局 */
@supports (display: grid) {
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: 24px;
}
.card-grid > * {
margin-bottom: 0;
}
}
/* 检测 subgrid 支持 */
@supports (grid-template-rows: subgrid) {
.card {
grid-template-rows: subgrid;
}
}
/* JS 侧的特性检测 */
if (CSS.supports('grid-template-rows', 'subgrid')) {
/* 启用 subgrid 方案 */
} else {
/* 回退到固定行高方案 */
}
二十三、性能:Grid 会影响渲染性能吗
这是一个常见疑问。简短的回答是:Grid 本身对渲染性能的影响非常小,真正影响性能的是网格的复杂度和元素数量。
/* 1. 轨道数量:轨道越多,布局计算越复杂 */
grid-template-columns: repeat(24, 1fr); /* 比 repeat(3,1fr) 慢 */
/* 2. fr 与 auto 的混合:需要多轮计算 */
grid-template-columns: auto 1fr auto 1fr; /* 比纯 1fr 慢 */
/* 3. 自动填充:auto-fit / auto-fill 需要测量容器 */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
/* 优化手段:给网格容器加 contain */
.grid-container {
contain: layout style;
}
/* 长列表:跳过屏幕外的渲染 */
.grid-item {
content-visibility: auto;
contain-intrinsic-size: auto 200px;
}
什么时候需要担心
- 超过 500 个网格项目:考虑虚拟滚动,只渲染可视区域内的元素。
- 网格尺寸随窗口变化频繁重算:给容器加
contain: layout style,避免影响外部。 - 网格布局本身在做动画:不要动画
grid-template-columns,它会触发布局。需要动画就用transform。 - 网格中包含大量图片:给图片设置明确的宽高比,避免布局抖动(CLS)。
.gallery img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
}
二十四、最佳实践清单与可复用代码
把全文的核心结论浓缩成一份可以贴在工位上的清单。
- 二维用 Grid,一维用 Flexbox:需要行列同时对齐就用 Grid,只是单行排列就用 Flex。
- 响应式卡片网格用
repeat(auto-fit, minmax(min(280px, 100%), 1fr)):一行顶十行媒体查询。 - 需要严格等分时用
minmax(0, 1fr):避免长内容把网格撑爆。 - 整页骨架用
grid-template-areas:代码即布局图,评审和维护都轻松。 - 响应式重排优先改 areas,而不是改 DOM 顺序:保住语义与无障碍。
- 间距一律用
gap:不要再用 margin 做网格间距。 - 网格项目默认
min-width: auto:需要防溢出时显式设置min-width: 0。 - 跨满整行用
grid-column: 1 / -1:不需要知道有多少列。 - 重叠元素直接用同一个
grid-area:不需要绝对定位。 - 卡片内部用 Flexbox:Grid 管外部排列,Flexbox 管内部结构。
- 长列表加
contain: layout paint:把重绘范围锁在局部。 - 图片加
aspect-ratio:避免加载时布局抖动。 - 慎用
grid-auto-flow: dense:它会打乱视觉顺序,影响无障碍。 - 不要动画
grid-template-*:布局属性动画代价极高。 - 用 DevTools 的 Grid 检查器:Chrome 可以直接高亮网格线与区域,调试效率翻倍。
/* 1. 响应式卡片网格 */
.grid-auto-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: 24px;
}
/* 2. 固定列数但严格等分 */
.grid-3col {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
}
/* 3. 侧边栏 + 主内容 */
.grid-with-aside {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 32px;
align-items: start;
}
@media (max-width: 768px) {
.grid-with-aside { grid-template-columns: 1fr; }
}
/* 4. 整页骨架 */
.page-shell {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
/* 5. 水平垂直居中 */
.grid-center {
display: grid;
place-items: center;
}
/* 6. 安全溢出:所有网格容器的通用防御 */
.grid-safe > * {
min-width: 0;
min-height: 0;
}
Grid 的学习曲线,其实比它看起来要平缓得多。真正需要跨过的坎只有三个:理解网格线的编号规则、记住 1fr 与 minmax(0, 1fr) 的区别、习惯用 grid-template-areas 描述整体结构。跨过之后你会发现,过去那些需要反复调整像素的布局,现在只需要改一行字符串。
建议你把这篇文章里的几个实验台反复玩几遍:拖动 fr 滑块观察比例变化,切换 auto-fit 与 auto-fill 看最后一行的差异,点击预设按钮感受 areas 的表达力。布局这件事,看十遍不如亲手改一次。当你能在脑子里“看见”网格线,并直接把想法翻译成几行 CSS 时,Grid 就真正属于你了。