Grid 网格布局这些年算是彻底翻身了。前几年大家还在为“垂直居中”折腾半天,现在随便打开一个后台系统、SaaS 产品界面,几乎都能看到 Grid 的影子。按社区里的说法,Flexbox 解决的是“一根绳子上的排列问题”,而 Grid 直接给你一张棋盘,让你从全局视角摆布页面里的每一个模块。标题里拿“下围棋”来比喻,我觉得很贴切:围棋的精髓在于“金角银边草肚皮”,先占角、再围边、最后争夺中腹;Grid 布局的核心也类似,先把页面切割成行列轨道,再把内容一一落位到交叉区域,整体感非常强。
这篇文章我会从 Grid 的底层心智模型讲起,结合我实际项目里的布局代码,把容器属性、项目属性、区域模板、隐式轨道这些概念逐一拆开,最后整理一份排坑清单。无论你是刚接触 CSS 的新人,还是用了很久 Flex 想拓展布局思路的开发者,都应该能从中找到可以直接抄作业的部分。
1. 为什么 Grid 能坐稳二维布局的王座
1.1 Flexbox 的天花板:一维轴的局限
在 Grid 成熟之前,Flexbox 是前端布局的绝对主力。它解决的是“沿着一根轴排列元素”的问题:水平排列、垂直排列、换行、对齐、拉伸,这些都是 Flex 的舒适区。比如一个导航栏里放五个链接,或者一个卡片底部放按钮组,用 Flex 写起来非常顺手。
但如果你仔细想过,会发现 Flex 在真正的大版面面前经常“使不上劲”。最常见的就是卡片列表:三列布局,行数不定,每一行还要保持卡片高度一致。用flex-wrap确实能让卡片换行,但换行之后,不同行的卡片之间没有“轨道”的概念——更准确地说,Flex 只关心主轴方向上的尺寸分配,副轴方向上虽然能用align-items控制对齐,但无法精确控制“这个元素占几行几列”。
这就逼着开发者做“数学题”:要三列,就手工给每个子元素设width: 33.333%;要留间距,再把百分比减去gap折算。一旦内容长度变化、容器宽度变化,这些“手算宽度”的方案就特别容易崩,窄屏上出现横向滚动条、卡片挤压错位,都是老生常谈的 Flex 换行问题了。
Grid 的诞生,本质上是把布局从“依靠元素自身宽度互相推挤”的模式,升级为“先定义网格轨道,再把元素放置到轨道交叉区”的模式。前者是元素驱动,后者是容器驱动。用生活类比来说,Flex 像在排队,每个人都往前挪;Grid 像在电影院对号入座,座位先画好,你只需要知道自己是几排几座。
1.2 Grid 的心智模型:从“排队”到“对号入座”
Grid 布局有两个核心角色:网格容器(grid container)和网格项目(grid item)。容器负责定义行和列,也就是生成“轨道”;项目则被放置在轨道切割出的单元格里。理解这套模型的关键,在于区分三条线:
- 网格线(grid line):分隔轨道的线,行与列都有各自的网格线编号,从 1 开始。
- 轨道(track):两条相邻网格线之间的区域,水平方向是“行轨道”,垂直方向是“列轨道”。
- 单元格(cell):行轨道与列轨道交叉形成的最小单位,和表格的单元格是一个概念。
用围棋来套:棋盘是 19 x 19 的轨道阵列,落子就是往交叉点放置项目。CSS Grid 里,你也能把项目“跨”多行多列,相当于一颗子占了一片区域,这在围棋里叫“势力范围”。
初学者最容易犯的思维错误,是把 Grid 当成表格的替代品。表格布局在语义上服务于“表格数据”,并且表格有自动撑开列宽的行为习惯;Grid 则是纯布局工具,列宽、行高、间距完全由你掌控。这也是为什么 Grid 比 Table 好用的地方——它不附带任何“单元格内容决定尺寸”的隐含逻辑,默认情况下轨道尺寸取决于你写进属性的数值,或者你声明的自动尺寸规则,而不是表格那种“内容多宽就多宽”的强制行为。
从操作系统的角度想,Grid 相当于给你的页面画了一个坐标系:X 轴定义列,Y 轴定义行,任何一片区域都可以精准定位。你不需要为了“让左边栏宽度固定,主区域填满剩余空间”而写一堆calc()或百分比,只需一行grid-template-columns: 240px 1fr,剩余的宽度自然交给1fr轨道。
2. 核心属性逐个击破:容器、轨道与项目定位
2.1 容器定义:display: grid与轨道生成
任何元素只要设置了display: grid,它的直接子元素就会自动变成网格项目,默认排列成单列多行。但单列显然不是我们想要的,所以真正干活的属性是grid-template-columns和grid-template-rows。
看一个最常见的后台布局:
.layout { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: auto 1fr auto; min-height: 100vh; }这三行 CSS 就把页面划分成了两列(左侧固定 240px 的侧边栏 + 右侧自适应宽度的主区域)和三行(顶部导航、内容区、底部)。auto在这里表示“由内容撑开高度”,而1fr表示“占据剩余空间的比例份额”。
fr单位是 Grid 里最精髓的设计。它跟%不一样,%是基于容器尺寸的百分比,总和不一定等于 100%;fr则是把剩余空间按比例分配。比如:
grid-template-columns: 1fr 1fr 1fr;这段代码定义了三个等宽的轨道,相当于各占三分之一。但如果某个轨道里面放了一个超长单词或者固定宽度的图片,1fr轨道会根据内容的最小尺寸自动调整,不会被压缩到不可读。
repeat()函数算得上高频使用:
grid-template-columns: repeat(4, 1fr);等价于1fr 1fr 1fr 1fr,在写卡片网格时特别省事。repeat()也支持更复杂的模式,比如repeat(2, 200px 1fr),意思是重复两次“200px 加一个弹性轨道”,在需要交替宽度的场景下很好用。
轨道尺寸确定后,别忘了轨道之间的缝隙。栅格系统的“沟槽”对应这里就是gap属性:
.layout { display: grid; gap: 16px; }gap是row-gap和column-gap的简写,同时控制行间距和列间距。我见过不少老代码还在用margin负数方案来模拟间距,到了 Grid 时代完全没必要了,gap不会产生额外的外边距折叠问题,也不会影响子元素的尺寸计算。
2.2 项目定位:grid-column与grid-row
容器定义好了,接下来就是把项目放到想去的位置。这里要用到网格线的编号。默认情况下,一个有 4 列的网格,网格线的编号是从 1 到 5。项目默认“进驻”第一个可用的行列位置,但你可以通过项目上的属性覆盖它的位置。
.item { grid-column: 2 / 4; /* 从第 2 条列网格线开始,到第 4 条结束 */ grid-row: 1 / 3; /* 从第 1 条行网格线开始,到第 3 条结束 */ }这里的2 / 4是“起始线 / 结束线”的写法。另一种更直观的写法是用span:
.item { grid-column: 2 / span 2; }意思是“从第 2 条列网格线开始,横跨 2 个轨道”,等价于2 / 4。我个人的经验是,代码可读性上span更好,尤其是别人看你的样式时,span 2直接传达“占两列”的意图,不需要数网格线。
网格线的编号还支持负数,从末尾倒数:grid-column: 1 / -1是“从第一列到最后一列”,常用于让某个全宽元素横跨所有轨道。
这部分就是栅格系统的底层原理。你在 Element 或 Ant Design 里用的Col span={8},本质上就是帮你生成grid-column: span 8(老版本基于 Flex 百分比另说)。理解了 Grid 的手写方式,以后再面对任何“组件库的栅格为什么这么设计”的疑问,答案已经在你脑子里了。
2.3 命名的力量:grid-template-areas与grid-area
网格线编号虽然精确,但抽象。你写grid-column: 2 / 4的时候,可能根本不记得第 2 条线是哪个区域的边界。特别是在复杂布局中,项目一多,写线号简直像查字典。
Grid 给你提供了“命名区域”这个更高层的抽象,用起来像在画示意图:
.layout { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "header header" "sidebar main" "footer footer"; } .header { grid-area: header; } .sidebar { grid-area: sidebar; } .main { grid-area: main; } .footer { grid-area: footer; }这个写法的直观程度拉满:grid-template-areas里的每一个字符串代表一行,每个名字代表一个区域,相同名字组成一个完整矩形。.代表空单元格。项目侧只需要一个grid-area: 名字就完成定位,不需要再关心线号。
这里有几个非常容易踩的坑,我分开说:
- 区域必须形成矩形:命名区域不能是 L 形或者其他不规则形状。比如你不能写成
"header header" "sidebar sidebar"然后又让主区域横跨两列的一部分,那会导致属性无效。想实现不规则布局,只能拆成更细的命名网格。 - 每一行的列数必须相同:
"header header"是两列,下面"sidebar main"也是两列,这没问题。一旦有一行写了三个名字,另一行写两个,整个grid-template-areas都会失效,项目直接回到自动排列状态。 - 空白字符串的坑:写法上,
"header header"中间的空白可以随意,只要名字对齐即可,但要注意字符串数量必须和grid-template-columns的轨道数一致。不一致时浏览器会忽略这个属性,而且不会有任何控制台报错,非常隐蔽。
我用grid-template-areas的时候,基本流程是:先在纸上画出页面的粗糙线框,然后直接照葫芦画瓢写成字符串。这个过程真的像在棋盘上先画势力范围,再去落子,对我这种“空间想象力一般”的人特别友好。
3. 从零搭一个页面:用网格下一盘完整的棋
3.1 需求场景与整体设计思路
下面我用一个具体的案例,把上面的属性串起来走一遍。场景是一个典型的内容型后台页面:顶部有导航栏,左侧是功能菜单,右侧是内容展示区,底部是版权信息。
页面拆解成四个区域:
- 顶栏:全宽,放 Logo 和用户信息。
- 侧边栏:固定 240px,放导航菜单。
- 主内容区:自适应宽度,放卡片和表格。
- 底栏:全宽,放版权说明。
如果把四个区域直接写成grid-template-areas,长这样:
.layout { display: grid; grid-template-columns: 240px 1fr; grid-template-areas: "topbar topbar" "sidebar main" "footer footer"; }等等,这里只有两列,顶栏和底栏跨两列,侧边栏和主内容区各占一列。听上去完美,但实际项目里主内容区内部还需要二次布局,比如卡片区域要三列自适应,表格要撑满全宽。所以我会把外层叫“骨架网格”,内层继续用 Grid 或者 Flex 搭“局部网格”。
这也是我反复强调的理念:一个 Grid 只负责一个层级的布局,不要试图把整个页面的所有细节都塞进同一个网格容器。你搭的是骨架,肌肉和纹理要留给子容器。
3.2 HTML 结构与完整 CSS 实现
HTML 结构简化为:
<div class="layout"> <header class="topbar">顶栏内容</header> <aside class="sidebar">菜单列表</aside> <main class="main"> <div class="card-list"> <div class="card">卡片1</div> <div class="card">卡片2</div> <div class="card">卡片3</div> <div class="card">卡片4</div> <div class="card">卡片5</div> <div class="card">卡片6</div> </div> </main> <footer class="footer">版权信息</footer> </div>CSS 骨架部分:
.layout { display: grid; min-height: 100vh; grid-template-columns: 240px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "topbar topbar" "sidebar main" "footer footer"; } .topbar { grid-area: topbar; } .sidebar { grid-area: sidebar; } .main { grid-area: main; } .footer { grid-area: footer; }侧边栏想固定宽度,同时允许单独滚动,可以给.sidebar加overflow-y: auto,但注意网格轨道本身在grid-template-columns里已经设了 240px,子元素内容超出时默认会影响轨道尺寸,除非给侧边栏容器设min-width: 0或者overflow: hidden。这是 Grid 新手最常见的疑惑:为什么我明明给了 240px,侧边栏还是被内容撑宽了?因为网格项目有一个自动最小尺寸(min-width: auto),内容多宽它就能撑多宽。要解决,给项目加min-width: 0,允许它缩小到轨道宽度。
主内容区的卡片列表,才是内部局部的重点:
.main { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; align-content: start; padding: 16px; }这段代码信息量很大,拆开讲:
repeat(auto-fill, minmax(240px, 1fr))的意思是:自动重复填入尽可能多的轨道,每个轨道的最小宽度是 240px,如果有剩余空间,就均分剩余空间让轨道变宽。- 容器宽度 800px 时,除以 240px,能放下 3 个轨道,加上
gap计算后,剩余空间分给这 3 个轨道,所以每个卡片约 256px 宽。 - 容器宽度 500px 时,只能放下 2 个轨道,卡片就变成 1fr 均分,每张约 242px。
- 容器宽度小于 500px 但大于 240px 时,仍然放 2 列,直到宽度小于 240px,才会降为 1 列。
这一步就赢过了传统手算百分比方案:你不需要写媒体查询,卡片列表会随着容器宽度智能调整列数。
align-content: start也值得解释一下。因为.main本身是grid-template-rows没有显式定义行的主内容区,内容高度可能小于容器剩余高度,默认情况下align-content是stretch,也即项目会被拉伸到占满整个内容区高度。一般情况下这没问题,但如果卡片列表只有两行,你会看到卡片被拉得很高,和设计稿不一致。设成start后,行轨道的高度等于内容高度,卡片不再被强制拉伸。
为了对比,我再给出一个使用 Flex 实现的同类卡片列表:
.card-list { display: flex; flex-wrap: wrap; gap: 16px; } .card { flex: 1 1 240px; max-width: 100%; }粗看效果差不多,但细节有差异:Flex 换行后,每一行内部的对齐与间距依赖justify-content,最后一行卡片少时会出现“左对齐但右侧留白”的正常效果,而 Grid 的行轨道是严格对齐的,最后一行卡片不足时,也会和上面的行共享相同的列宽。排版上 Grid 更整齐,但也更需要明确每张卡片的宽度策略。
实际项目中我是这么分工的:整体框架用 Grid,导航菜单、按钮组、卡片内部细节用 Flex。比如顶栏里的用户头像和下拉菜单,用display: inline-flex; align-items: center; gap: 8px;就能搞定;但顶栏本身作为网格的一个区域,的位置归属由 Grid 决定。
3.3 响应式:媒体查询与改道
网格骨架在窄屏上往往需要重新设模板。最直接的方式是媒体查询里覆盖grid-template-columns和grid-template-areas:
@media (max-width: 768px) { .layout { grid-template-columns: 1fr; grid-template-areas: "topbar" "main" "sidebar" "footer"; } }这个写法相当于把侧边栏从左侧挪到主内容下方。为什么放在下方而不是上方?从用户浏览习惯讲,手机屏幕宽度有限,用户更关心主内容,导航菜单放底部或者用抽屉触发更合理。用grid-template-areas改道,比 Flex 里调整order属性直观得多——你就是在换棋盘上的区域排布。
如果你追求更高级的“免媒体查询响应式”,可以尝试给容器直接设置:
.layout { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-areas: "topbar topbar" "sidebar main" "footer footer"; }但注意,repeat(auto-fill, ...)在这里不会自动把“侧边栏和主内容”重新排成一行,因为grid-template-areas写死了每一行的区域数。所以“完全免媒体查询”不太现实于这种大骨架布局,骨架级的响应式还得靠媒体查询换模板。内层卡片列表才是auto-fill大显身手的舞台。
4. 常见问题排查与避坑经验
4.1 为什么轨道会“凭空”多出来
用grid-template-columns定义了 3 列,但容器里放了 5 个项目,第 4、5 个项目并不会报错,也不会溢出,而是自动在网格底部生成新行。这些没有显式定义尺寸的行就叫“隐式轨道”。
隐式轨道的尺寸由grid-auto-rows控制,默认是auto,也就是按内容高度。有时这会导致不是你想要的效果,比如弹窗下方多出很长的空白。解决办法是显式声明:
.grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 160px; gap: 12px; }grid-auto-rows: 160px让所有自动生成的行动轨道固定为 160px 高,多出来的项也会按照规律排列,整体版式看起来沉稳许多。反过来,如果你希望自动创建的列固定宽度,用grid-auto-columns。
排查隐式轨道的一个小技巧,就是打开浏览器开发者工具的 Grid Overlay,直接看网格线编号。如果看到的编号比grid-template里定义的轨道数多,那多半就是隐式轨道在起作用。
4.21fr和auto、minmax()怎么选
1fr经常被误解成“等分内容”,其实它是“等分剩余空间”。剩余空间的计算逻辑是:先减去所有固定尺寸轨道(px、%、auto)和gap,再把剩下的按比例分给fr轨道。所以下面两种写法的效果完全不同:
/* 写法A */ grid-template-columns: auto 1fr; /* 写法B */ grid-template-columns: 1fr 1fr;写法A里,第一列宽度由内容决定(如侧边栏里的长菜单文本),第二列吃掉所有剩余空间;写法B里,两列均分剩余空间,如果第一列内容特别长,它会把第二列挤压到内容最小宽度以下吗?不会,因为fr轨道也有最小内容尺寸的限制,但整体上两列的起点位置和宽度分配逻辑就不一样。
想要兼顾“固定最小宽度”和“剩余空间分配”,用minmax():
grid-template-columns: minmax(200px, 1fr) minmax(320px, 2fr);这个布局的意思是:第一列至少 200px,第二列至少 320px,剩余空间按 1:2 分配。这个写法非常适合实现“左侧图标列 + 右侧内容列表”的复合布局,比手写max-width和百分比要稳得多。
我自己的选型习惯是:
- 需要固定宽度(如侧边栏 240px、头像 48px)直接用
px。 - 需要由内容撑开宽度(如顶栏里的一组标签)用
auto。 - 需要吸收剩余空间、占据比例(主内容区、卡片列表的弹性列)用
1fr或minmax(..., 1fr)。
提示:不要让所有轨道都是
1fr或minmax,至少保留一列固定宽度,否则网格在极端窄屏下会出现内容挤压,视觉上很不舒服。
4.3auto-fit与auto-fill到底差在哪
repeat(auto-fill, ...)和repeat(auto-fit, ...)是卡片列表响应式的两兄弟,但行为有一点关键差异。
auto-fill:容器宽度足够时,会填满所有可能轨道数,即使某些轨道没有项目,也会保留轨道位置。所以“空的列”还会占位。auto-fit:容器宽度足够时,会自动折叠掉那些空轨道,让已有项目拉伸,占据整个容器宽度。
看个小示例:
/* 容器宽 700px,项目最小宽 200px */ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));容器能放下 3 个轨道(200 * 3 + 2 * gap = 632px 左右)。如果只有 2 个项目,auto-fill会保留第 3 列空轨道,两个项目在左,右侧留白;auto-fit会折叠第 3 列,把两个项目拉伸并填满整行。
实际上的处理规则还要看有没有剩余空间和1fr的配合,但理解了“空轨道保留/折叠”语义后,用起来基本不会出错。做卡片列表时,我默认选auto-fit,更优雅;做日历、课程表这种需要严格占满格子的场景,auto-fill更内聚。
4.4 利用开发者工具调试网格
写 Grid 布局时,我几乎离不开浏览器开发者工具的 Grid Overlay。以 Chrome DevTools 为例,选中一个设置了display: grid的容器元素,在 Elements 面板里,元素标签旁边会出现一个网格徽标。点击它,页面就会叠加显示网格线、轨道编号和区域名称。
调试grid-template-areas时更有用:它会用不同颜色的半透明区块标出每个命名区域,你能一眼看出哪块区域发生了重叠、哪个区域没被正确匹配。边缘的网格线编号也能辅助确认grid-column: 2 / 4到底跨到哪。
如果你发现某个项目没有出现在预期位置,先打开 Overlay 看编号,再确认项目的grid-row/grid-column是否写错了方向(把 row 写成了 column)。这种错误比语法错误更难发现,因为浏览器不报错,只是把项目放到自动排列的某个位置,视觉上表现为“乱序”,但 Grid Overlay 会帮你还原整个棋盘。
还有一条经验:grid-template-areas失效时,项目会退回自动排列,此时 Overlay 显示的区域里没有任何名字。遇到这种情况,优先检查字符串行列数是否和轨道数一致,以及是否有重复命名的非矩形区域。
最后分享一个我自己的习惯变化。早几年做页面,我都是先写 DOM,然后 Flex 一把梭,遇到对齐问题再调 justify 和 align,改来改去效率很低。现在我做任何“有骨架感”的页面,第一件事是打开一个绘图工具或者就在脑子里画一个大方框,先确定分成几行几列,哪些区域要跨列,哪些区域要固定宽度,再用grid-template-areas落到代码里。这就像是先把棋盘画好,后面每个模块的落位都清楚。
至于 Grid 和 Flex 的关系,我一直觉得不是二选一。Grid 管大棋盘,Flex 管棋盘内部的棋子排列,两者配合用才是现代 CSS 布局的完全体。如果你目前只习惯 Flex,建议给自己一个小挑战:下一次做后台布局或者卡片网格时,强制用 Grid 写一遍,踩几次隐式轨道和min-width的坑之后,基本就回不去了。