最近打开编辑器想写点布局相关的东西,顺手看了一眼搜索热度,“grid布局阮一峰”又挂在前面,后台管理系统的布局、Dashboard搭建、各种响应式页面的问题底下,问来问去最后还是绕回到Grid身上。这玩意儿从2017年浏览器大规模支持到现在,早就不再是“新特性”了,而是和Flexbox一样属于前端的基本功。但基本功恰恰是最容易被低估的,很多人嘴上说会用,实际一写flex一行搞定,grid能写出花来却不知道怎么落笔。
这篇文章我不打算做API文档的搬运工,而是把我自己从“知道Grid”到“真的用Grid干活”这个过程里的思路、代码、坑和选择,完整地梳理一遍。说得直接一点:Flexbox适合解决一维的排队问题,而Grid解决的是二维的地盘划分问题——就像下围棋,不是看单个棋子怎么走,而是看整张棋盘怎么布局。这篇内容适合三种人:刚学完Flexbox想进阶的、做后台系统整天写重复布局的、以及想让自己页面布局更清晰可维护的同学。
1. 为什么说Grid是二维世界的布局王者
1.1 一维布局的尽头,就是二维网格的起点
很多教程喜欢用“一维vs二维”来讲Flexbox和Grid的区别,这句话对,但不够准确。Flexbox的本质是“一条轴上的排列”,主轴加交叉轴,听起来是二维的,但它的核心控制能力集中在主轴方向,交叉轴上的对齐只是锦上添花。你可以让一排按钮横着排,也可以让一列卡片竖着排,但如果你想让某个元素同时占据两行两列,Flexbox就非常别扭了。
Grid就不一样,它的模板声明天然就是两个维度同时展开的。你用grid-template-columns定义列,用grid-template-rows定义行,行和列交叉产生的格子,本身就是二维的坐标系。这在处理整页布局时极其顺手:顶部导航横跨所有列,侧边栏占一列并且从上到下,主内容区占剩余空间,底部栏横跨全局——这样的结构,用Flexbox写不是不行,但需要嵌套多层,而且一旦某个区域的高度变化,其他区域很难联动。
我用一个实际的例子来说明。经典的三栏布局带顶栏和底栏,如果不用Grid,常规做法是外层flex竖排,中间区域再嵌套flex横排,再给侧边栏定宽,给主区域定flex:1,这已经算比较顺的了。但如果你还需要让侧边栏内部是“标题+列表+底部按钮”三段结构,主区域内部又是“工具栏+表格+分页”三段结构,那么每个区块内部又要开新的flex容器,嵌套层级直接失控。
换成Grid之后,顶层只需要一句grid-template-areas,整页的骨骼就定死了,每个区域的内部再自己去操心自己的内容排列。这个思维的转换,才是“一维到二维”真正的意义:Flexbox是排列内容的工具,而Grid是先划分地盘再安排内容的工具。
1.2 围棋隐喻:全局视角、落子定位与网格天性
标题里我用围棋来比喻Grid,不是因为“下围棋”听起来高级,而是这个类比真的能让你理解Grid的设计哲学。围棋的第一步不是落子,是先看棋盘——19乘19的交叉点,你得先有地盘意识,才知道往哪儿落子。Grid也一样,写第一行CSS之前,得先在脑子里把页面切成若干块,想清楚每块占几行几列,谁和谁对齐,谁是固定尺寸谁是弹性伸缩。这一切都是“网格天性”,页面结构天然就是网格化的。
围棋落子讲究“占位”,一个棋子落在交叉点上,它周边的势力范围就变了。Grid里的grid-column和grid-row就是这个落子动作。你可以写grid-column: 1 / 3,让一个项目从第1条网格线跨到第3条网格线,这在视觉上就是“这个区块吞掉了两个格子的宽度”。你不需要关心DOM顺序,不需要用float去绕,也不用给每个区块写死margin,只需要按坐标放进去,它就待在那里。
我以前带过一个团队,新来的同学一上来就喜欢用flex把首页拼出来,结果产品要求某一块区域改成跨两列,他改了半个小时的HTML结构,加了三层wrapper才搞定。我告诉他:这就是你一开始没有用Grid的代价。一个真正二维的布局系统,关心的是“每个区域落在棋盘的哪个位置”,而不是“这些DOM节点怎么排队”。一旦你接受了这个视角,再看任何复杂页面,脑子里会自然而然地冒出网格线。
1.3 顺带提一嘴搜索词的问题
看到热搜里有“off grid ai”这个词,顺带说一句。这个热词和CSS Grid其实没有太大关系,更多是AI生成的页面布局概念混淆。我猜很多人搜这个是想搜“CSS Grid布局”,结果被AI自动生成的错误内容带偏了。
实际上,Grid的“off grid”可以理解成“脱离常规文档流,但要按网格坐标放置”。这个理解放在CSS里其实挺好用:Grid项目已经不在普通文档流里排列,而是按照网格算法、或者是用户指定的行列坐标来摆放。所以聊到Grid的时候,把“网格”和“脱离文档流”这两件事放在一起理解,你就抓住了它的骨架。
2. 先搞懂这几个概念,再动手写代码
2.1 容器与项目:先有棋盘,再有棋子
Grid布局最少需要两类角色:网格容器和网格项目。容器是设置了display: grid的那个元素,项目的直接子元素就是网格项目。注意,是“直接子元素”,孙元素不算,它不会自动参与当前网格的排布,除非你再给孙元素的父级开一个Grid容器。这个嵌套关系,跟Flexbox完全一样,所以熟悉flex的同学不会陌生。
我从实际项目里发现一个很普遍的问题:新手常把display: grid加在了错的元素上。比如要给一组li卡片做网格排布,有人会把grid加在ul的父级div上,结果发现子元素全变成了一列。这里的判断标准很简单:谁想要排列子元素,谁就是容器。li们要被排成多列多行,那容器就是ul;ul本身要和其他区块组成页面,那容器就是更外层的div。每一层的关系都按这套逻辑去套,就乱了。
容器的直接子元素会成为网格项目,但这不意味着它必须是div。p、span、a、button都可以充当网格项目,只要它是直接子元素就会被网格系统接管。我在做数据大屏的时候,经常直接用一堆section来充当网格项目,区域划分清晰,语义化也还在,并不需要额外套一层div。
2.2 网格线、网格单元、网格区域
在Grid的世界里,有三样基础构件:网格线、网格单元和网格区域。网格线是编号的线条,列方向从1开始从左往右数,行方向从1开始从上往下数,相邻两条线夹出来的区域就是网格单元。
这个编号机制非常重要,因为grid-column和grid-row都是通过网格线来定位的。比如四列网格,就有5条列线,你要让一个项目占满从左边数第2列到第4列,就写grid-column: 2 / 5(或者用span写法grid-column: 2 / span 3)。这个坐标思维跟围棋的星位如出一辙,你落子不是落到单元格上,而是按线来占位。
网格区域是多个相邻网格单元的集合,也就是若干个格子拼成的一个矩形。区域可以自己命名,然后通过grid-template-areas用“画图”的方式直接布局。这一招在处理整页骨架时简直是降维打击。
我见过太多人只会用数字定位,从来不写grid-template-areas。确实,数字定位更灵活,但要考虑一个问题:页面的区域如果后期换位置,你改的坐标要同步改多处,代码的可读性会比较差。而grid-template-areas直接是把页面的“缩略图”写在了CSS里,看到代码的人立即就明白页面的结构。
2.3 显式网格与隐式网格:你画的棋盘和自动生成的格子
几乎每个Grid新手都会遇到一个困惑:我明明写了三列,但子元素有五个,后面两个跑到哪儿去了?答案是它们被放进了隐式网格。
显式网格,是你通过grid-template-columns和grid-template-rows明确画出来的那些轨道;隐式网格,是当项目数量超过预设轨道、或者有项目被硬塞到未定义的位置时,浏览器自动生成的额外轨道。自动生成的轨道默认宽度是auto,也就是说它由内容撑开,很可能和你预设的轨道尺寸不一致,从而造成视觉上的跳跃。
这个坑在实际项目中非常常见。我举个例子:你设置了grid-template-columns: repeat(4, 1fr),每行放四个卡片,卡片循环渲染出来一共八个,像素级完美。但如果你渲染的是九张卡片,第九张就会掉到第二行的末尾,这一行的宽度同样是四列1fr,所以看起来问题不大。真正出问题的是竖向排列的卡片流,你只定义了grid-template-rows的第一行高度,后面自动生成的行高没有设置,结果高度参差不齐。
解决办法是提前用grid-auto-rows来给隐式网格定一个统一的行高。比如我需要卡片网格里每一张卡片高度一致,就写grid-auto-rows: 200px,这样所有自动生成的轨道都有了确定的高度。这个属性属于“平时想不起来,出问题时才想起”的类型,我先放在这里,后面实战章节还会再提。
3. 上手实操:从零到一写出第一个Grid布局
3.1 第一步:定义容器和列
先用最简单的场景开胃。一个普通的内容区,希望一行放三列,代码长这样:
.grid-container { display: grid; grid-template-columns: 200px 1fr 1fr; gap: 16px; }这里说明一下为什么这样写。200px是固定尺寸,可以用于左侧的固定侧栏或者图片封面位;两个1fr是剩余空间的比例分配。fr这个单位是Grid的弹性单位,它的计算逻辑是:先把固定尺寸(px、%、em等)扣掉,再把剩余空间按fr比例分给弹性轨道。这个机制很像Flexbox里的flex: 1,但更直观,因为它是针对轨道本身的。
很多人一开始不理解fr和%的区别,我打个比方。你用%是按容器的宽度百分比来分配,用fr是按除去固定轨道后的剩余空间来分配。如果容器宽度是1000px,有grid-template-columns: 200px 50% 1fr,那么第二列是500px,第一列200px,第三列只有300px。但如果你写成grid-template-columns: 200px 3fr 1fr,那第三列就不是300px了,而是(1000 - 200) * (1 / (3+1)),也就是200px。这个差别在你调整容器宽度时会非常明显,fr布局天然会自适应剩余空间,而%布局会和固定宽度产生冲突后溢出。
3.2 第二步:定义行、间距
列搞定了,行也要有个交代。Grid的两条轴是可以独立控制的,你可以只定义列,让行自动生成,也可以定义行,让列自动生成。但是如果你希望页面在纵向也受到控制,就得用grid-template-rows:
.grid-container { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 100px 1fr; gap: 12px 24px; }这段代码表示两行两列,第一行高度固定100px,第二行占据剩余高度。gap是我平时最常用的一个属性,它替代了老掉牙的margin方案,两个值分别是行间距和列间距。以前写网格布局要小心翼翼地给每个元素设置margin,再把最后一个的margin清零,用Grid之后这堆破事全免了。
间距还有一个容易踩的细节:gap只作用于网格轨道之间,它不会在容器边缘额外加间距。所以如果你的设计稿要求网格整体距离容器四周也有间距,需要在容器上加padding,或者用整个容器外围的布局去兜住。
3.3 第三步:把项目放进网格
定义完轨道之后,子元素会按DOM顺序自动依次填充每一个网格单元。如果想让某个项目精确落位,就要用到grid-column和grid-row:
.item-sidebar { grid-column: 1 / 2; grid-row: 1 / 3; } .item-main { grid-column: 2 / 4; grid-row: 2 / 3; }这里我把四个列定义成了三列(实际上代码里有两列,这只是一个示范,具体数字按你的模板来),侧边栏占据第一列,行方向从第1条线到第3条线,所以它纵向跨了两行。主内容区占据第二列到第四列,行方向从第2条线到第3条线。这种写法在flex里几乎做不到,因为它天然不具备“跨行跨列”的能力。
在写数字定位的时候,可以用span关键字,语义更清晰:grid-column: 2 / span 2意思是从第2条线开始,跨过2个网格单元。我在复杂的项目中更喜欢用span写法,因为2 / 4这种写法一旦列数变了,你要回去数线,而span写法告诉你的是“我要占多少个格子”,更抗改动。
3.4 第四步:用grid-template-areas做可视化排布
如果说数字定位是“坐标法”,那grid-template-areas就是传说中的“画图法”。它允许你直接用字符拼出一个区域的缩略图:
.app { display: grid; grid-template-columns: 200px 1fr; grid-template-rows: 60px 1fr 40px; grid-template-areas: "header header" "sidebar main" "footer footer"; } .app-header { grid-area: header; } .app-sidebar { grid-area: sidebar; } .app-main { grid-area: main; } .app-footer { grid-area: footer; }这是后台管理页最经典的一段布局,看到代码里的人一眼就能明白:顶部横跨两列,左边是侧边栏,右边是主区域,底部再横跨两列。完全不需要注释,也不需要去数网格线编号。
grid-template-areas的字符有几个规则要注意:每个模板字符串代表一行,每个单词代表一个网格单元;同一个区域名可以重复出现多次,但必须是矩形连通的,不能是L形、田字形以外的不规则形状;用.(点号)表示该位置不放项目,留空。
我早期做项目时总是把网格模板字符串写得花里胡哨,后来才发现对齐的可读性才是王道。尽量用大写字母或者单词缩写,并且每一行的字符数保持一致,否则模板一长就容易错位。
3.5 第五步:响应式里的auto-fill、auto-fit与minmax
响应式是Grid最帅的表演场地,不用写媒体查询就能实现很多自适应效果。
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }这一行代码的意思是:每一列的最小宽度是280px,最大是1fr;浏览器根据容器宽度自动决定一行放几列。容器越宽,列数越多,卡片始终保持在合理宽度范围内。
auto-fill和auto-fit是另一个容易被忽视的坑。简单说,auto-fill会尽量多地创建空轨道,即使没有内容填进去;auto-fit会把空轨道折叠掉,让有内容的轨道伸展填满整行。如果你想让卡片在只有一张时也能铺满整行,用auto-fit;如果你希望即使没内容也保留轨道的视觉占位,用auto-fill。这个差别在空网格和不足一行的场景里尤其明显,我做后台空状态页面时经常用auto-fit,让一个孤零零的卡片撑满宽度,视觉上不尴尬。
minmax()这个函数非常关键,它不只是设置宽度范围,还能和fr配合形成“最小可接受尺寸+弹性扩展”的轨道逻辑。比如minmax(200px, 2fr),表示这条轨道至少200px,多余空间按2fr的权重参与分配。这比单纯%或fr要可靠得多,因为它给轨道设了一条底线。
4. 实战场景:三种常见页面的Grid解法
4.1 场景一:后台管理页的经典三区布局
后台管理系统几乎长一个样:顶部工具栏、左侧菜单、右侧内容。用Grid的解法在上一小节已经展示过grid-template-areas,但实际项目里还藏着一些小问题。
比如左侧菜单的内容高度可能超过视口,右侧内容也可能整体很长。此时你需要决定整个页面滚动还是区域内滚动。如果整页滚动,Grid的外层高度用min-height: 100vh,同时把主区域设置成min-height: 0,否则内容会把网格轨道撑开,部分区域被挤变形。这个问题在flex布局里也存在,但在Grid里表现得更隐蔽,因为轨道默认min-height: auto,内容长了它就会自动撑开,看起来好像布局没问题,实际上早已溢出。
我再给你们一个亲测有效的组合:
.app { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "header header" "sidebar main" "footer footer"; height: 100vh; } .app-main { min-height: 0; overflow-y: auto; }顶部和底部高度由内容决定,中间区域占满剩余空间,内部滚动,侧边栏天然和主区域等高。这种“一面墙”的效果,是我做后台项目最常用的骨架。
4.2 场景二:12栅格系统与内容卡片
12栅格一直是从Bootstrap时代流行下来的布局方案,以前用flex配合栅格类名来写,现在用Grid可以轻松实现等价的栅格系统,但代码更简单。
.row { display: grid; grid-template-columns: repeat(12, 1fr); column-gap: 16px; } .col-4 { grid-column: span 4; } .col-6 { grid-column: span 6; } .col-8 { grid-column: span 8; }这种方式的好处是,你不再需要写一排带着margin和width的工具类。哪怕产品突然要求某个卡片从占用4列改成占用5列,你只需要改一个grid-column类名,不需要去调整其他任何元素。
而且Grid的栅格系统还比flex多一个优势:跨行。比如某个内容卡片需要同时占3列和2行,直接给grid-column: span 3; grid-row: span 2就完成了。这在flex栅格系统里几乎不可能实现,必须嵌套容器去模拟。
4.3 场景三:Dashboard卡片式布局
数据大屏和Dashboard是Grid的高频使用场景,因为这种页面天生就是零散的卡片拼成的,不同卡片高低错落、宽度不一。以前的解法是position: absolute加手工计算像素,或者上复杂的第三方拖拽库。其实很多时候Grid就够了。
.dashboard { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: 80px; gap: 16px; } .card-wide { grid-column: span 6; grid-row: span 2; } .card-tall { grid-column: span 3; grid-row: span 4; } .card-square { grid-column: span 3; grid-row: span 2; }注意这里出现了grid-auto-rows,我前面提到过的隐式网格控制属性。Dashboard的这些卡片高度往往不是由内容决定的,而是由设计稿定死的节奏来划分的。grid-auto-rows: 80px会把每一行轨道都固定成80px,这样grid-row: span 2的卡片高度就是160px加一个间隙,视觉效果齐整得像网格系统一样稳定。
另一个Dashboard常用技巧是用grid-column让卡片跨不同列数,做出不对称的砖墙效果,同时保持网格线对齐。这种效果如果用flex去做,大小卡片混排会留下大量“洞”,要手动调margin去补齐,想想都头大。
5. 常见问题与避坑排查实录
5.1 属性写在了错误的对象上
网格布局的属性分为容器属性和项目属性两类,这是新手最容易搞混的地方。display: grid、grid-template-columns、grid-template-rows、gap只写在容器上;grid-column、grid-row、grid-area、justify-self、align-self只写在项目上。
如果写了grid-column但没有生效,先检查这个元素是不是容器的直接子元素。如果它被包在了一层div里,它就不再是网格项目,属性自然无效。此时要么调整HTML结构让它是直接子元素,要么给中间层加display: contents让它的内容“穿透”到当前的网格体系里来。display: contents是个冷门但很好用的属性,相当于把这个元素的盒模型抹掉,让它的子元素直接参与外层网格。
5.2 隐式网格的尺寸异常
前面反复提过隐式网格的问题,这里我给出一个完整的排查思路。当你的项目数量超出了显式网格的容量,或者项目被放置到未定义的行/列,浏览器就会自动生成隐式轨道。这个轨道的大小默认是auto,受内容影响很大,容易和显式轨道的尺寸不一致。
如果你遇到了“前几行整齐,后面几行乱掉”的情况,先检查grid-template-rows是否只定义了前几行的尺寸,比如只定义了grid-template-rows: 100px 200px,后面的行都会自动生成。解决办法是按需补充grid-auto-rows,或者手动调整模板行的范围。
5.3 子项目在Grid里的尺寸行为
Grid项目默认会填满它所放置的网格单元区域,但这不等于它的宽度和高度是100%。比如一个按钮放在网格单元里,它的宽高默认是auto,但align-self和justify-self的默认值是stretch,所以它会拉伸填满单元格。如果某个项目不想被拉伸,需要设置justify-self: start或align-self: center。
还有一点容易被忽略:网格项目默认min-width: auto,当内容超出单元格宽度时,项目会撑破轨道,而不是自动缩回。如果你想阻止内容把轨道撑破,需要给项目设置min-width: 0。这个知识点在滚动容器和长文本场景中非常关键。
5.4 Grid和Flexbox到底怎么选
我做一个简单的分流判断。如果布局的核心是一维排列、沿线对齐,用Flexbox,比如导航栏里的按钮分布、块级容器内部的水平排列。如果布局的核心是二维划分、跨行跨列、区域面积分割,用Grid,比如整页骨架、卡片网格、Dashboard。
好的页面往往是两者混用的:外层用Grid划分大区块,区块内部再用Flexbox排列内容。比如侧边栏是Grid里划出来的一块区域,而侧边栏内部菜单的排列用Flexbox来做。这种“Grid管大局、Flex管细节”的用法,是我在实际项目中验证过的最高效的组合。
5.5 兼容性与周边坑
Grid的浏览器兼容性已经非常成熟,主流浏览器全面支持。需要注意的点更多在于细节属性的支持差异,比如subgrid在部分场景下支持不完整,复杂项目里要谨慎使用。另外,gap在Grid里完全没问题,在Flexbox里现代浏览器也支持了,但老项目里如果还要求老旧浏览器,需要确认一下。
日常开发中,我发现更多人栽在代码层级和语义结构上,而不是兼容性上。比如把Grid容器和Flex容器层层嵌套,又不注意min-height: 0的传递,最后出现莫名其妙的滚动条。排查时从最外层开始,逐层确认轨道伸缩、内容撑开和溢出,基本都能找到问题。
6. 写在最后的一点个人体会
我自己从Flexbox跨到Grid,经历了大概两周的别扭期,总觉得Grid“太啰嗦”,写一行布局要定义那么多东西。真正让我改变习惯的,是一次后台重构:原来的页面嵌套了五层flex容器,模块位置调整一次要动三处HTML、两处CSS,我花了一天改成了Grid骨架,整个结构清爽得就像在一张白纸上画分镜图。从那以后,凡遇到“区域划分”的活儿,我第一反应就是Grid。
最后再分享一个小技巧:在Chrome开发者工具里,给Grid容器选中网格叠加层,能直接看到网格线和轨道编号,定位错乱时先开这个覆盖层瞅一眼,比我上面写的所有排查步骤都快。这个功能是我调试Grid layout最依赖的利器,没有之一。