CSS Grid 精通:从二维布局到实战
Flexbox 是一维的,Grid 是二维的——真正的页面布局该用 Grid。本文讲清 Grid 的核心概念(行/列/区域/轨道)、fr 单位与 minmax、grid-template-areas 语义化布局、响应式 Grid(auto-fit/auto-fill)、Grid vs Flexbox 的取舍,带你从"能用 Grid"到"精通 Grid"。
本文要点
- 主题聚焦:React、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
CSS Grid 精通:从二维布局到实战
"Flexbox 排一行,Grid 排一张表。"
很多开发者写了好几年 React,布局还是清一色的 Flexbox:display: flex 一把梭,外层 flex、内层 flex、嵌套 flex 套娃。能跑吗?能。优雅吗?不。
页面骨架这种典型的"二维布局"——同一时刻你既要控制行又要控制列——用 Flexbox 写出来基本就是一堆嵌套 + 一堆 flex-grow + 一堆 media query 凑出来的"补丁墙"。
CSS Grid 是 2017 年正式落地的布局标准,它是浏览器里第一个真正为二维布局设计的系统。一旦你建立起了 Grid 的心智模型,90% 的"响应式布局难题"会直接消失。
这篇文章不讲 API 罗列,只讲心智模型 + 实战取舍。读完你应该能:
- 看着设计稿,脑子里直接浮出
grid-template-* - 知道什么时候用 Grid,什么时候用 Flex
- 写出零 media query 的自适应卡片网格
- 用
grid-template-areas写出"看一眼 CSS 就懂页面结构"的语义化布局
一、为什么是 Grid:二维布局的本质
理解 Grid 的第一步,是放下 Flexbox 的"一维"直觉。
Flexbox 是一维的:一次只能沿一个方向(主轴)排列。就算你 flex-wrap: wrap 让它换行,你也无法控制第二行有几个、每行怎么对齐——那是浏览器自己决定的。
Grid 是二维的:你可以同时显式控制行和列。每个子项的位置都可以用"第几行第几列"精确描述,行与列之间是平等的、对称的。
看这张对比图就懂了:
Flexbox(一维) Grid(二维)
┌──────────────────────┐ ┌─────┬─────┬─────┐
│ [A][B][C] │ │ A │ B │ C │
┌──────────────────────┐ ├─────┴──┬──┴─────┤
│ [D][E] ← 换行了, │ │ D │ E │
│ 但你控制不了 │ │ │ │
└──────────────────────┘ ├────────┴────────┤
│ F │
└─────────────────┘
一切靠主轴 行列同时控制
什么时候该用 Grid?一句话:只要你的布局在视觉上呈现为"表格状"或"卡片状"——也就是同一行有多个对齐的列,且跨行也有对齐关系——就用 Grid。
具体场景:
- 整页骨架(header / sidebar / main / footer)
- 仪表盘卡片墙
- 图库 / 商品列表
- 表单的双列布局(label | input)
- 任何"行+列都要对齐"的 UI
反过来,组件内部一行水平排列的按钮、一行图标 + 文字——这些是 Flex 的主场,别强行用 Grid。
二、核心属性:grid-template 与轨道(Track)
Grid 的核心就一组属性:在容器上定义网格结构,子项自动落入格子。
2.1 三个最常用的容器属性
.grid {
display: grid;
grid-template-columns: 200px 1fr 1fr; /* 三列:固定 + 两份弹性 */
grid-template-rows: auto 1fr auto; /* 三行:内容高 + 弹性 + 内容高 */
gap: 16px; /* 行列间距,简洁版是 row-gap/column-gap */
}
这里出现了一个核心概念:轨道(Track)。一条轨道就是网格里"一整列"或"一整行"。grid-template-columns 定义的就是列轨道的宽度序列。
布局示意:
grid-template-columns: 200px 1fr 1fr;
grid-template-rows: auto 1fr auto;
col1(200px) col2(1fr) col3(1fr)
┌───────────┬───────────┬───────────┐
r1 │ │ │ │ auto(内容撑开)
├───────────┼───────────┼───────────┤
r2 │ │ │ │ 1fr(吃掉剩余空间)
│ │ │ │
├───────────┼───────────┼───────────┤
r3 │ │ │ │ auto
└───────────┴───────────┴───────────┘
gap = 16px
2.2 fr 单位:Grid 的灵魂
fr(fraction,分数)是 Grid 引入的弹性单位。它表示把剩余空间按份数分配。
/* 三列等宽 */
grid-template-columns: 1fr 1fr 1fr;
/* 侧边栏 + 主区,2:5 的比例 */
grid-template-columns: 240px 5fr; /* 240px 固定,剩下全给主区 */
/* 经典的 1:2:1 */
grid-template-columns: 1fr 2fr 1fr;
关键认知:fr 分配的是剩余空间,不是总空间。如果某列内容撑开了,固定列优先,fr 列分剩下的。这和 Flexbox 的 flex-grow 一脉相承,但 Grid 里写起来更直观。
2.3 minmax:Grid 真正的弹性之美
光有 fr 还不够——1fr 在内容很少时会塌成 0,在内容很长时会被撑爆。minmax() 解决这个问题:
/* 这一列最小 200px,最大 1fr */
grid-template-columns: minmax(200px, 1fr) 3fr;
minmax(min, max) 让轨道有"弹性区间":不会小于 min,不会大于 max。这是后面响应式 Grid 的基石。
2.4 repeat():少写点废话
/* 不写 */
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
/* 写 */
grid-template-columns: repeat(6, 1fr);
/* 混合写 */
grid-template-columns: 200px repeat(3, 1fr) 100px;
repeat(n, size) 就是语法糖,但配合下一节的 auto-fit 才是它的真正威力。
三、grid-template-areas:语义化命名布局
这是我个人认为 Grid 最被低估的特性。它让你的 CSS 读起来就像一张 ASCII 画的布局图。
3.1 用名字摆出整个页面
.layout {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 60px 1fr 50px;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
gap: 12px;
}
.layout > header { grid-area: header; }
.layout > aside { grid-area: sidebar; }
.layout > main { grid-area: main; }
.layout > footer { grid-area: footer; }
看 grid-template-areas——它就是页面的形状。你不用看 HTML,光看 CSS 就知道这个页面长什么样:
┌─────────────────────────────┐
│ header │
├──────────┬──────────────────┤
│ │ │
│ sidebar │ main │
│ │ │
├──────────┴──────────────────┤
│ footer │
└─────────────────────────────┘
子项只要写一句 grid-area: xxx 就归位了。不用算行号、列号,不用 span,不用 grid-column: 1 / 3 这种"算术题"。
3.2 几条规则记住就行
- 每个名字必须形成一个矩形区域(不能是 L 形、T 形)。
- 要让某块跨多列/多行,就把名字重复写多次(上面 header 跨两列就是这么写的)。
- 用
.表示"这个格子空着"。 - 每一行的格子数必须相等。
3.3 配合 media query 重排布局
这是 grid-template-areas 最爽的用法——响应式重排只需要改一片字符串,HTML 完全不动:
/* PC:左侧栏 */
@media (min-width: 768px) {
.layout {
grid-template-columns: 220px 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
}
/* 移动端:侧栏沉到底部 */
@media (max-width: 767px) {
.layout {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
JSX 里就一句:
<div className="layout">
<header />
<aside />
<main />
<footer />
</div>
这是我做项目时最常推荐给团队的 Grid 用法。它的可读性吊打任何行号计算。
四、响应式 Grid:auto-fit / auto-fill + minmax
接下来这一节,是 Grid 最"魔法"的部分——写一行 CSS,做出无 media query 的自适应卡片墙。
4.1 自适应卡片网格:一行 CSS
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
这一行的效果:
- 视口宽 → 自动放下更多卡片,每张至少 240px
- 视口窄 → 自动减少列数,每张仍保持 240px 以上
- 完全不需要 media query
示意(视口从宽到窄):
视口 1200px:每张 240px+,放 4 列
┌────┬────┬────┬────┐
│ C1 │ C2 │ C3 │ C4 │
└────┴────┴────┴────┘
视口 800px:放 3 列
┌─────┬─────┬─────┐
│ C1 │ C2 │ C3 │
└─────┴─────┴─────┘
视口 500px:放 2 列
┌────────┬────────┐
│ C1 │ C2 │
└────────┴────────┘
原理拆解:
minmax(240px, 1fr):每列最小 240px,最大吃掉剩余空间auto-fit:浏览器自己算能放几列,塞满容器repeat():把上面那个配方重复 N 次,N 由浏览器算
4.2 auto-fit vs auto-fill:一个关键区别
这两个值 90% 时候行为一样,只有在卡片数少于能放下的列数时才表现出差异:
容器能放下 4 列,但只有 2 张卡片
auto-fill: auto-fit:
┌────┬────┬────┬────┐ ┌──────────┬──────────┐
│ C1 │ C2 │ │ │ │ C1 │ C2 │
└────┴────┴────┴────┘ └──────────┴──────────┘
保留空的轨道位置 把空轨道折叠,拉伸现有卡片
auto-fill:始终保留轨道占位,卡片"靠左排",剩下位置空着。auto-fit:把空轨道折叠成 0,让现有卡片拉伸占满整个容器。
实战经验:99% 的卡片列表场景(图库、商品、博客)用 auto-fit——卡片少的时候让它们撑满,看起来才不"瘪"。auto-fill 更适合"占位等待填充"的场景,比如分类导航的固定格子。
4.3 为什么这比 media query 强
经典 media query 写法的痛苦:
.cards { grid-template-columns: 1fr; }
@media (min-width: 540px) { .cards { grid-template-columns: 1fr 1fr; } }
@media (min-width: 800px) { .cards { grid-template-columns: 1fr 1fr 1fr; } }
@media (min-width: 1080px) { .cards { grid-template-columns: repeat(4, 1fr); } }
四个断点,每次窗口大小变了还得调。minmax + auto-fit 一行搞定,而且响应是连续的,不是阶梯式跳变。
五、子项放置:span / line number / named lines
容器定义好网格后,默认情况下子项按顺序一个格子放一个。但 Grid 的真正强大在于:每个子项都能精确声明自己占哪里、占几格。
5.1 三种放置方式
方式一:用网格线编号(line number)
Grid 自动给每条线编号,从 1 开始。列线从左到右 1,2,3...,行线从上到下 1,2,3...
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.item-a { grid-column: 1 / 3; } /* 从线1到线3,跨2列 */
.item-b { grid-row: 2 / 4; } /* 从线2到线4,跨2行 */
1 2 3 4
┌─────┬─────┬─────┐
1 │ A │ │
├─────┼─────┼─────┤
2 │ │ │ │
│ B │ │ ← B 从行线 2 到行线 4
3 │ │ │ │
└─────┴─────┴─────┘
4
/ 前是起点线,/ 后是终点线。终点是线不是格子,所以 1 / 3 占的是格子 1 和格子 2(两条线之间是一格)。
方式二:用 span(跨几格)
不想算终点线?用 span:
.hero { grid-column: span 2; } /* 跨 2 列 */
.banner { grid-column: 1 / span 3; } /* 从线1开始跨3列 */
.tall { grid-row: span 4; } /* 跨 4 行 */
这是仪表盘里最常用的写法——大卡片跨 2 格、小卡片跨 1 格。
方式三:命名网格线
在 grid-template-* 里用 [名字] 给线打标签:
.layout {
grid-template-columns: [sidebar-start] 200px [sidebar-end main-start] 1fr [main-end];
}
.sidebar { grid-column: sidebar-start / sidebar-end; }
.main { grid-column: main-start / main-end; }
可读性比纯数字好得多,配合 grid-template-areas 使用可以做到"全场零数字"。
5.2 z 顺序与层叠
Grid 的格子可以重叠——多个子项放到同一格,靠 z-index 决定谁在上。这意味着你可以用 Grid 做覆盖式布局(比如带遮罩的 hero 图):
.hero {
display: grid;
}
.hero img { grid-area: 1 / 1; } /* 占据整块 */
.hero .overlay { grid-area: 1 / 1; } /* 同一格,盖在上面 */
.hero .cta { grid-area: 1 / 1; align-self: end; }
三元素全挤到 1 / 1,自然层叠。这是 Grid 比 Flex 灵活的地方之一。
六、Grid vs Flexbox:何时用哪个
这是新人最常问的问题。一句话原则:
页面骨架用 Grid,组件内部用 Flex。
更细的判断:
| 场景 | 用什么 | 为什么 |
|---|---|---|
| 整页 / 大区块布局 | Grid | 二维,行列同时控制 |
| 仪表盘卡片墙 | Grid | auto-fit + minmax 一行搞定 |
| 表单 label/input 两列 | Grid | 严格对齐两列 |
| 一行按钮 / 导航项 | Flex | 一维水平排列 |
| 图标 + 文字 | Flex | 内容驱动,不需要对齐到外层列 |
| 居中一个元素 | Flex(一行) | Grid 也行,但 Flex 更轻 |
| 文本截断 + 尾部按钮 | Flex | min-width: 0 配合 flex 更顺 |
心智模型:
- Flex 想:"让这些东西沿一条线排,谁多谁少由 flex-grow 决定"。
- Grid 想:"整个区域是个棋盘,每个东西放在第几行第几列"。
它们不是互斥的——实际项目里通常是外层 Grid 容器,每个 Grid 子项内部再用 Flex 布局自己的小内容。两层职责清晰,不存在"二选一"。
举个例子:
<div className="dashboard"> {/* Grid:定义卡片墙 */}
<div className="card">
<div className="card-head"> {/* Flex:标题 + 操作按钮一行 */}
<h3 />
<button />
</div>
<div className="card-stats"> {/* Flex:3 个数字横排 */}
<span />
<span />
<span />
</div>
</div>
</div>
外层用 Grid 把卡片墙摆好,内层用 Flex 处理卡片里的一行内容。这就是生产环境的正确姿势。
七、实战三例
7.1 圣杯布局(Holy Grail)
经典中的经典:header、footer、中间三栏(左 nav、中 main、右 aside)。
.holy-grail {
display: grid;
grid-template-columns: 200px 1fr 240px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
min-height: 100vh;
gap: 16px;
}
.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";
}
}
JSX:
<div className="holy-grail">
<header />
<nav />
<main />
<aside />
<footer />
</div>
对比一下用 Flexbox 写同样的布局——你需要嵌套、order、media query 里改 flex-direction、再加一堆 flex-basis。Grid 版本短一半,可读性高一个量级。
7.2 仪表盘网格(不等分卡片)
仪表盘的典型需求:有几个大卡片(KPI 趋势图),几个小卡片(数字、状态),它们要拼成一张墙。
.dashboard {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 140px; /* 每行默认高 140px */
gap: 16px;
}
.kpi-big { grid-column: span 2; grid-row: span 2; } /* 2x2 大卡 */
.kpi-wide { grid-column: span 2; } /* 横长卡 */
.kpi-tall { grid-row: span 2; } /* 竖高卡 */
.kpi-small { /* 默认 1x1 */ }
效果示意:
┌──────────────┬──────────────┬──────┬──────┐
│ │ │ S1 │ S2 │
│ kpi-big │ kpi-wide ├──────┴──────┤
│ (2x2) │ │ kpi-tall │
│ ├──────┬───────┤ (1x2) │
├──────────────┤ S3 │ S4 │ │
│ kpi-wide │ │ │ │
└──────────────┴──────┴───────┴─────────────┘
关键技巧:grid-auto-rows: 140px 让每行固定高,这样 span 2 的卡片高度就是 140 * 2 + gap,跨行卡片之间天然对齐。这是仪表盘布局的核心套路。
7.3 自适应卡片列表(响应式神器)
最常用、最实用的一个——博客文章列表、商品墙、图库都靠它:
.card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.card {
/* 卡片内部用 Flex 处理垂直方向的内容 */
display: flex;
flex-direction: column;
padding: 20px;
border-radius: 8px;
}
.card-body {
flex: 1; /* 让所有卡片底部对齐 */
}
export function ArticleList({ articles }: { articles: Article[] }) {
return (
<section className="card-list">
{articles.map(a => (
<article key={a.id} className="card">
<h3>{a.title}</h3>
<p className="card-body">{a.excerpt}</p>
<a href={`/a/${a.slug}`}>阅读全文</a>
</article>
))}
</section>
);
}
这里有一个进阶技巧:用 flex: 1 让 .card-body 撑开。这样不管某张卡片的标题/摘要多长,"阅读全文"按钮永远在卡片底部对齐——视觉上整齐划一。
同一行卡片,标题长度不一:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 短标题 │ │ 这是一个非 │ │ 中等标题 │
│ │ │ 常长的标题 │ │ │
│ │ │ │ │ │
│ 摘要内容... │ │ 摘要内容... │ │ 摘要内容... │
│ │ │ │ │ │
│ │ │ │ │ │
│ → 阅读全文 │ │ → 阅读全文 │ │ → 阅读全文 │ ← 全部对齐
└──────────────┘ └──────────────┘ └──────────────┘
这就是外层 Grid + 内层 Flex 的经典配合——Grid 管宏观排列,Flex 管微观内容。
7.4 加餐:CSS subgrid(子网格)
如果你需要让嵌套的子网格和父网格共享列轨道(比如表格状的多列卡片),传统 CSS 做不到——子项一旦进 Grid 容器就独立成网格。subgrid 解决了这个问题:
.parent {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.child {
display: grid;
grid-template-columns: subgrid; /* 继承父网格的列定义 */
grid-column: span 3;
}
2024 年起主流浏览器(Chrome 117+、Safari 16+、Firefox 71+)已全部支持。生产环境可以放心用——它能解决"嵌套表格卡片"这种 Grid 以前的痛点。
小结
回到本文开头的判断,带你看完 Grid 的全貌后,三条要点拎清楚:
-
Grid 是二维系统,Flex 是一维系统。 页面骨架、卡片墙、对齐表这类"行列都要管"的布局,应该首选 Grid。一行按钮、图标+文字这种内容驱动的一维排列,留给 Flex。
-
minmax(min, 1fr) + auto-fit是响应式布局的核武器。 一行 CSS 实现"卡片随视口自适应列数、无 media query"。99% 的卡片列表场景用auto-fit(不是auto-fill)——它能让卡片少的时候撑满容器。 -
grid-template-areas让 CSS 自己讲清楚页面结构。 命名区域、ASCII 风格的布局声明、媒体查询里换一片字符串就能重排——这是 Grid 最被低估、最该团队推广的特性。
3 条实战经验:
- 能用
span就别算行号。grid-column: span 2比grid-column: 1 / 3可读、好维护。 - 跨行卡片用
grid-auto-rows固定行高。 仪表盘对齐的秘密就这一句。 - 外层 Grid、内层 Flex。 大结构用 Grid 摆,小内容用 Flex 排——职责分明,互不冲突。
金句:Flexbox 解决"这一行怎么排",Grid 解决"这一页怎么布"。把它们当成两件不同的工具,而不是一个的替代品——你的 CSS 会瞬间清爽一截。
下一篇我们聊 Container Queries——元素级别的响应式,它会再次颠覆你写 media query 的习惯。
Related