
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 classlayout header classtopbar顶栏内容/header aside classsidebar菜单列表/aside main classmain div classcard-list div classcard卡片1/div div classcard卡片2/div div classcard卡片3/div div classcard卡片4/div div classcard卡片5/div div classcard卡片6/div /div /main footer classfooter版权信息/footer /divCSS 骨架部分.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-areasmedia (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的坑之后基本就回不去了。