CSS Grid 网格布局是我这几年用得最顺手、也最后悔没早学的一套二维布局方案。早些年遇到整页骨架、多列区块,我全是拿 Flexbox 叠嵌套,三层之后改一个间距要翻半天代码。Grid 的出现把“行”和“列”同时交到你手里,一句话就能让元素跨两行、占三列,定位逻辑直白得像个表格。这篇我会把 Grid 的底层逻辑、核心属性、高频布局写法和踩坑经验一次性整理清楚,适合刚过完 CSS 基础、想系统补齐布局能力的开发者,也适合用过 Grid 但一直“会用不会讲”的同行。
1. 先从“为什么”说起:Grid 到底解决了什么问题
1.1 从一维到二维:Grid 的思路转变
过去我们用传统布局做页面骨架,最常见的姿势是 float + clearfix,后来进化到 Flexbox。Flexbox 的核心模型是一条主轴加一条交叉轴,它设计的初衷是解决“一排元素怎么排列、怎么对齐”的问题。这句话翻译过来就是:Flexbox 本质是一维布局工具,它擅长处理一条线,当你需要同时控制行和列、让一个元素占住“第2列且跨2行”时,Flexbox 就会非常别扭,通常得在外层包容器、算宽度百分比、甚至用负 margin 去补齐间隙。
Grid 的思路是直接拿出一个二维网格:你把容器想象成一张 Excel 表格,先定义有几列、每列多宽,再定义有几行、每行多高。里面的每一个子元素不再靠“浮动到左边”或者“挤进同一行”的方式排列,而是直接通过行列编号或命名区域站到指定位置。这种从“一维顺序流”到“二维坐标定位”的转变,是 Grid 和之前所有布局方案最本质的区别。
我刚接触 Grid 时也犯过一个错误:总想把 Grid 和 Flexbox 对立起来。其实它们不是互斥关系,而是分工关系。Grid 负责的是整体骨架,也就是页面级或者模块级的大结构;Flexbox 负责的是小范围内的排列,比如导航栏里几个链接的水平分布、按钮组里图标的垂直居中。实战里最常见的是“Grid 容器里某个区域的内容用 Flex 来排”,两者嵌套使用,反而最舒服。
1.2 与 Flexbox 的分工:别再傻傻分不清
很多新手纠结“这个布局该用 Grid 还是 Flexbox”,我的判断标准很简单:如果只需要考虑一个方向,比如一行按钮、一列表单,用 Flexbox;如果同时要考虑两个方向,比如产品卡片网格、后台管理系统的页面框架,用 Grid。
举个直观的例子:一个商品列表,要求每行 4 个商品、商品高度尽量一致,这就是二维问题。用 Flexbox 做,你得给每个 item 设宽度为 25%,还得注意 gap、wrap、最后一行不足 4 个时的拉伸问题;用 Grid 写,grid-template-columns: repeat(4, 1fr)一行代码就搞定,列数、宽度分配、对齐全部由网格系统接管,而且项目的默认行为就是填充整个网格区域,高度天然等齐。
我整理过一张简化版对比表,方便日常选型时快速判断:
| 对比点 | Flexbox | Grid |
|---|---|---|
| 维度 | 一维,适合单方向排列 | 二维,行列同时控制 |
| 主要场景 | 导航、按钮组、表单行 | 页面骨架、卡片墙、混合布局 |
| 定位方式 | 靠主轴排列和自动换行 | 靠网格线编号或命名区域 |
| 列宽控制 | 依赖 flex-basis / width | 直接定义轨道尺寸 |
| 元素跨行跨列 | 比较困难,需要额外结构支撑 | 原生支持,非常方便 |
这张表不是标准答案,而是给选择困难症一个快速抓手。实际项目里经常是两者一起用,Grid 画出“地图”,Flexbox 负责地图里每个房间内的家具摆放。
2. 容器属性:先学会把格子画出来
2.1 第一行代码:display: grid 与轨道定义
任何 Grid 布局的第一步,都是给父容器设置display: grid。这一步并不会立刻改变子元素的排列方式——如果只设置这一行,所有子项会按照默认的单列网格排列,因为还没有定义列轨道。
真正划分网格的是grid-template-columns和grid-template-rows。看一个最基础的例子:
.container { display: grid; grid-template-columns: 200px 1fr 2fr; grid-template-rows: 100px auto; }这段代码的意思是:把容器分成 3 列,第一列固定 200px,第二列和第三列按剩余空间分配,其中第三列分到的空间是第二列的 2 倍。行方向分成两行,第一行固定 100px 高,第二行高度由内容自动决定。这是 Grid 的第一个核心观念:轨道尺寸是显式定义的,子元素数量不够多时轨道也会存在。
关于这里的1fr,我多说一句。fr是 fraction(分数)的缩写,可以理解为“剩余空间的一份”。1fr不是固定宽度,而是等分剩余空间。比如grid-template-columns: 1fr 2fr,容器如果有 600px 宽,先不管有没有固定列,剩余部分会被分成 3 份,第一列拿 1 份,第二列拿 2 份。这种按比例分配的方式,比手动计算百分比省心得多,尤其在做响应式布局时,容器宽度一变,比例自动重新计算。
2.2 轨道尺寸与 fr 单位的背后逻辑
很多人会把1fr直接等同于25%或flex: 1,其实有细微差别。fr在分配空间时,会先考虑内容的最小宽度约束。默认情况下,grid-template-columns: 1fr 1fr的两个列,如果某列里的文字特别长且不换行,它可能会被内容撑破,导致另一列被压缩。这是我在项目里碰到过的非常典型的坑,后面第 5 节会专门讲怎么解决。
轨道尺寸除了fr,还可以混用px、em、rem、%、auto、minmax()。我的经验是把单位按职责分开:
| 单位/关键字 | 适用场景 | 注意点 |
|---|---|---|
| px / rem | 固定尺寸区域,如侧边栏宽度 | 不会随容器伸缩 |
| 百分比 | 相对容器宽度的列 | 注意与 gap 叠加时可能溢出 |
| auto | 由内容决定尺寸 | 行为类似 fit-content |
| fr | 弹性分配剩余空间 | 注意内容最小宽度可能撑破比例 |
| minmax() | 设定最小和最大边界 | 结合 fr 使用非常强大 |
关于百分比和fr的区别,有一个实际体验:面板网站左侧 220px 导航、右侧内容区,这种情况下左边用固定值,右边用1fr。如果左右都用百分比,比如左 20% 右 80%,一旦容器宽度变化或者加入固定 gap,右侧很容易出现横向滚动条,因为百分比是按内容盒计算的,fr的剩余空间分配会主动考虑gap,这是它更稳的核心原因。
2.3 minmax 和 repeat:让网格自适应
repeat()函数是写网格轨道的“快捷方式”。最常用的两种写法是:
.container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }上面代码等价于1fr 1fr 1fr。更灵活的是repeat(auto-fill, minmax(220px, 1fr))这种组合,也是我写响应式卡片列表时最常用的一行魔法。
minmax(220px, 1fr)的意思是:这个轨道的最小宽度是 220px,最大可以拉伸到 1fr 所能分配的空间。auto-fill的语义是“让浏览器尽可能多地创建轨道”,如果容器宽度足够放下 3 个 220px 的列,就创建 3 列,如果不够,就减少列数,让每一列宽度至少保持 220px。这样根本不需要写媒体查询,容器从 320px 到 1920px 都能自动适配。
还有一个容易混淆的兄弟叫auto-fit。两者区别在于容器宽度较大、项目数量不足以填满所有轨道时的表现:auto-fill会保留空白轨道,空轨道宽度依然占据空间;auto-fit会把空轨道折叠为 0,让现有的项目自动拉伸填满整行。我举个实例:一个一行显示 8 个图标的工具栏,希望图标始终靠左排列,用auto-fill合适;一个商品列表,希望无论几个商品都尽量撑满整行展示,用auto-fit更合适。
2.4 gap 与对齐属性的组合运用
当年用 float 布局时,列间距要小心翼翼地在每个 item 上写 margin,到了 Flexbox 时代有了gap,Grid 里同样有,而且建议优先使用它,因为它不改变子元素自身的盒模型,也不用担心给最后一列多加右边距。
.container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px 16px; }这里gap: 20px 16px表示行间距 20px、列间距 16px。只写一个值时,行和列间距相同。注意gap负责的是轨道和轨道之间的间距,容器边缘不会因为这个属性产生内边距,想留白还需要用padding。
再补齐容器内的两组对齐属性。第一组是justify-items和align-items,控制每个项目在自己的网格区域内水平/垂直方向的对齐方式,默认是stretch,也就是项目默认会填满整个网格区域,这也是 Grid 做等高列毫无压力的原因。如果你不想让项目被拉伸,可以设置justify-items: start或place-items: center让项目居中。第二组是justify-content和align-content,控制的是整个网格在容器中的位置,前面说了,默认网格会占满容器,所以这两兄弟在“网格总宽度小于容器”时才有明显效果。
3. 项目属性:让元素按计划站到指定位置
3.1 line-based 定位:用编号精确控制跨行跨列
容器建好网格后,子元素默认按源文档顺序自上而下、从左到右一个个填充网格。但 Grid 真正强大的地方在于,你可以手动指定某个元素从第几条网格线开始、到第几条网格线结束。
每个网格的列边界和行边界都有编号,从 1 开始。默认情况下,从左往右列编号递增,从上往下行编号递增。所以:
.item { grid-column: 1 / 3; grid-row: 1 / 2; }这段代码表示当前元素占据第 1 列到第 3 列(即跨越前两列),同时占据第 1 行。grid-column和grid-row都是缩写形式,完整写法是grid-column-start、grid-column-end、grid-row-start、grid-row-end。我平时几乎只用缩写,因为它们读起来非常直观。
除了用数字,还可以用负数。-1永远指向最后一条网格线,所以grid-column: 1 / -1可以很方便地让某个元素横跨整行,这是做头部、底部的常用技巧。还有一个关键字span,可以写作grid-column: span 2,意思是不关心从哪开始,只要求占据 2 列的宽度,这对自适应数量的网格非常友好。
3.2 grid-template-areas 区域命名法
数字编号在简单的网格里很好用,但要维护一个复杂的页面骨架时,满屏的grid-column: 1 / 3阅读成本很高。此时用grid-template-areas就像画图一样,直接给每个区域命名。
看一个实际的后台管理页面骨架:
.container { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: 60px 1fr 40px; grid-template-areas: "header header" "sidebar main" "footer footer"; height: 100vh; } .header { grid-area: header; } .sidebar { grid-area: sidebar; } .main { grid-area: main; } .footer { grid-area: footer; }grid-template-areas里每一行字符串对应网格的一行,同一个名字出现的位置就是该区域占的位置。header连续出现两次,代表它横跨两列;“.”则代表空单元格。这种方式最直观的优势是:布局结构一眼可见,后续要调整哪个区域的位置,直接改字符串排列,不用动子元素的任何属性。
我用这种方式重构过好几个老项目,最大的感受是代码可读性提升明显。新同事接手时不用问“第二列第三行是哪个元素”,打开 CSS 看几行字符就懂了。缺点是区域形状必须规整,必须是矩形,不能一个区域跨出 L 形或 T 形。
3.3 place-self 与对齐细节
单个项目想在网格区域内居中或者贴边,可以用justify-self和align-self。它们的作用范围是当前项目所在的网格区域,不会影响其他格子里的元素。写起来也一样有缩写:
.item { justify-self: center; align-self: end; } /* 等价于 */ .item { place-self: center end; }place-self的第一个值是垂直方向(align-self),第二个值是水平方向(justify-self),顺序不要记反。我在实际开发中,最常用的是把某个 Grid 子项设置为place-self: center,让它在所属网格区域正中间展示,省去了单元素 flex 包一层的麻烦。
这里还要提一个容易忽略的属性order。Grid 项目默认按 DOM 顺序排列,如果你希望视觉顺序和源码顺序不同,比如为了首屏渲染效率把主要内容放在 HTML 前面、视觉上又想让侧边栏先显示,可以修改order属性。注意,order改变的是视觉排列顺序,不会改变区域命名定位的结果。区域定位优先级更高,两者结合时要先理解你到底是想要“流动填充”还是“精准站位”。
4. 实操:三种高频布局一次讲透
4.1 页面骨架:三行两列控制台布局
后台管理、数据看板这种页面通常骨架高度一致:顶部固定导航,左侧侧边栏,右侧内容区,底部版权栏。用 Grid 实现非常清晰:
<div class="layout"> <header class="header">顶部导航</header> <aside class="aside">侧边栏</aside> <main class="main">内容区</main> <footer class="footer">底部</footer> </div>.layout { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: 56px 1fr 48px; grid-template-areas: "header header" "aside main" "footer footer"; height: 100vh; }在 HTML 里,四个子元素按 header、aside、main、footer 的顺序书写,但通过grid-template-areas的命名,它们的实际位置完全由字符串控制。main区域会自动占据侧边栏右侧的剩余空间,高度由1fr撑满中间行。这里有一个经验:height: 100vh让整个网格撑满视口,页面内部的内容再滚动,而不是整个 body 滚动,这种结构对后台系统来说体验更接近桌面应用。
想要让侧边栏和内容区内部自己做滚动,可以给main设置overflow: auto,给aside设置overflow-y: auto。因为网格轨道已经把高度分配好了,这两个区域内部滚动不会影响整体骨架。这个方案我用了很久,比传统的 float + 手动计算高度稳定得多。
4.2 响应式卡片列表:auto-fill + minmax
商品卡片、图片画廊、文章列表,是 Grid 最闪光的场景之一。我最精简的实现只用了三行核心代码:
.card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 20px; }容器宽度变化时,列数会在 1 到 N 之间自动切换。比如容器宽度是 1040px,减去 gap 之后每列最小 240px,浏览器会自动算出可以放 4 列;宽度缩到 800px 时,可能就变成 3 列。整个过程不需要任何媒体查询。
这个写法的关键点在minmax(240px, 1fr):最小 240px 保证了卡片在小屏设备上不会缩太小;最大1fr保证了空间富裕时每列能等分剩余空间,不会出现右侧大片空白。如果你用的是repeat(4, 1fr),那 240px 的最底限就得靠媒体查询补,远不如这个写法省事。
需要注意auto-fill和auto-fit的区别在这个场景下有实际影响:如果你有一行里项目不够 4 个,比如只有 3 个商品,auto-fill会用空轨道填满第 4 列位置,每个商品宽度反而不变;auto-fit会把第 4 列折叠掉,3 个商品平分整行、每个更宽。实际产品里想要哪种效果,最好先跟设计确认,不要随手选一个。
4.3 经典圣杯布局的 Grid 写法
圣杯布局指的是页面从上到下分成头、中、尾,中间部分再分成侧边栏+主内容,整体要求等高。这个布局用 float 和 flex 都能做,但 Grid 最直观,也最容易扩展到复杂变体。
.holy-grail { display: grid; grid-template: "header header" 80px "nav main" 1fr "footer footer" 60px / 200px 1fr; min-height: 100vh; } .holy-grail > header { grid-area: header; } .holy-grail > nav { grid-area: nav; } .holy-grail > main { grid-area: main; } .holy-grail > footer { grid-area: footer; }这里用了grid-template简写,一次性定义区域、行高和列宽:引号里的内容对应grid-template-areas,每行引号后面的尺寸对应行高,最后面的/ 200px 1fr定义列宽。这种写法比分开写三个属性更紧凑,读代码时一眼就能看出整体布局比例。
我经常用一个技巧是给中间区域也做嵌套 Grid:main里再放一个display: grid,把内容区继续细分。这样外层管页面骨架、内层管业务卡片,层级清晰,维护时不用翻很远去改样式,对大型项目非常有价值。
5. 高频踩坑与排查实录
5.1 1fr 为什么比自动宽度更调皮:最小宽度溢出
这是被问得最多的问题:为什么我用grid-template-columns: 1fr 1fr,第一列却被内容撑开,第二列被挤没了?原因是1fr轨道的默认最小值是auto,也就是内容的最小宽度。如果第一列里有一张很宽的图片或一段不换行的长英文,轨道会被内容撑到比预期更宽,剩余空间分配也随之乱套。
解决方法是把最小值显式写成 0:
.container { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }这样轨道的最小宽度从“内容最小宽度”变成 0,固定在轨道内的内容如果超出,会通过溢出滚动/隐藏来处理,而不是把轨道本身撑破。凡是遇到“Grid 列不知道为什么比预期宽”的情况,先检查是不是忘了用minmax(0, 1fr),八成是这个原因。
顺带提醒一个同类问题:gap加上固定百分比轨道时,也容易出现溢出。比如两列各占 50%,再加gap: 16px,总宽度变成了100% + 16px,容器出现横向滚动条。此时要么改用fr,要么把百分比改成calc((100% - 16px) / 2)。用fr永远是更省心的选择,这个结论我写代码时几乎不会被推翻。
5.2 隐式轨道:行高和列宽悄悄变了的元凶
Grid 有一个“隐式轨道”的概念。我们通过grid-template-columns和grid-template-rows定义的轨道叫显式轨道,但当你放置的子元素超出了显式轨道的范围,浏览器会自动创建新的轨道来容纳这些项目,这些新轨道就是隐式轨道。
比如你定义了 3 列,但放了 5 个子元素,第 4、5 个元素会自动撑出新的行轨道。问题在于,这些隐式轨道的尺寸不由grid-template-rows控制,默认是auto,高度完全由内容决定。所以在做页面骨架时,你明明给行设置了1fr,一旦元素被放错位置或数量增多,就会多出一行看不见的隐式行,把布局撑乱。
排查方法很简单:给容器加上grid-auto-rows和grid-auto-columns,统一定义隐式轨道尺寸。比如:
.container { grid-template-columns: repeat(3, 1fr); grid-auto-rows: minmax(120px, auto); }这样任何多出来的行都会遵循统一规则,不会出现某个行列高忽大忽小的问题。grid-auto-flow也很关键,默认是row,即按行方向自动排列项目;改成column后项目会优先填列,配合grid-auto-columns做纵向瀑布流会顺手很多。
5.3 Grid 项目里的 Flex 子元素会不会占位异常
Grid 容器内部的项目如果需要继续排列内部小元素,我们经常让项目本身也变成 Flex 容器。这本身没毛病,但要注意此时项目在 Grid 里的默认拉伸行为。Grid 项目的默认align-items是stretch,所以项目会填满网格区域;当项目内部用display: flex时,如果项目没有被撑满,Flex 子元素的尺寸计算可能和你预期的不一样。
举一个真实场景:Grid 的每个卡片区域里有一个按钮,按钮希望贴在卡片底部。如果卡片本身被内容撑得很高,按钮贴底需要卡片也是 Flex 容器,并且让按钮的margin-top: auto。这段逻辑没有 Grid 参与也能成立,但很容易让人误以为是 Grid 的问题。
排查这类问题,我建议先把父容器的display: grid临时改成display: block,看看项目自身的定位和尺寸是否正常。如果正常,问题多半出在轨道尺寸或对齐属性上;如果还不正常,再去项目内部排查。用排除法可以很快缩小范围。
5.4 调试工具和几个我常用的速查习惯
推荐每个做布局的人都养成用浏览器 Grid Inspector 的习惯。DevTools 里选中带display: grid的元素,会显示网格线、轨道尺寸和区域名称,实时查看每个项目落在哪个格子,比自己在那盲猜效率高一倍不止。Chrome、Firefox 现在的网格调试工具都已经很成熟,Firefox 甚至可以同时高亮多个网格。
我个人的速查习惯是:写 Grid 时先在注释里画出结构图,比如:
/* header | header menu | content footer | footer */然后把grid-template-areas按这个结构填进去。这样上手速度极快,也不容易写错区域名。还有一个习惯是给所有 Grid 容器明确设置min-width: 0或min-height: 0,很多时候子元素溢出并不是 Grid 本身的问题,而是因为默认的最小尺寸限制没有放开,提前写好可以省掉很多后续定位的麻烦。
6. 几个可以直接抄作业的 Grid 技巧
6.1 用 grid-auto-flow: dense 消除布局空洞
当你在一个网格里手动指定了某些项目跨行跨列,且有些项目没有指定位置时,自动排列有可能在局部留下空白。加上grid-auto-flow: dense后,浏览器会尝试把后面的项目回填到这些空白里,视觉上更紧凑。这个属性做图片墙、标签列表时特别好用,缺点是会改变部分项目的视觉顺序,如果对 DOM 顺序要求严格,慎用。
6.2 配合容器查询完成更细粒度的响应式
Grid 和容器查询是近年响应式合拍的一对。容器查询让你可以基于组件父容器的宽度来调整内部布局,而不是基于视口宽度。结合 Grid 的minmax,可以实现组件级自适配。这类写法没有太多新知识,只需要把repeat(auto-fill, minmax(...))的容器包在一个可查询容器里,卡片的列数变化就会只跟它所在的区域宽度挂钩,而不会被页面尺寸影响。
6.3 用命名网格线降低维护成本
除了给区域命名,网格线也可以命名。写法是:
.container { display: grid; grid-template-columns: [sidebar-start] 220px [main-start] 1fr [main-end]; }项目定位时可以写作:
.content { grid-column: main-start / main-end; }这比记忆第几根线更可读,尤其在网格结构比较复杂、后续可能插入新列的情况下,数字编号容易因为新增列而全部错位,命名网格线的容错性更高。如果项目只是简单三五行,用数字完全足够,不必为命名而命名。
这套布局思维我用了几年之后最深的体会是:Grid 不是一个需要背属性的功能点,而是一种用“空间”思考页面的方式。你越早把脑海里的布局从“一行行往下排”切换成“一张表、几个区、若干个格子”,写出来的样式就越稳定。希望这篇能帮你把 Grid 那张一直没捅破的窗户纸捅开,后面实际项目写多了,你会发现自己再也不想回到层层嵌套的老路上。