我做页面布局做了这么多年,从表格布局一路用到 Flexbox,再到现在的 CSS Grid,感受最深的一点是:Grid 就是为网页排版量身定制的二维布局神器。它跟我们以前用的任何布局方案都不一样,因为它从底层就把"行和列"当成了第一公民。如果你还在靠 Flexbox 加各种百分比、calc、负 margin 去硬拼复杂排版,那这篇文章就是为你准备的。
这篇文章不会只贴一段官方文档式的语法说明,我会从实际项目出发,把 Grid 的核心概念、关键属性、真实场景案例和我在实战中踩过的坑都整理出来。无论你是刚接触 Grid 的初学者,还是已经用了一阵子但总觉得没吃透的开发者,都能在这里找到可以直接照搬的写法和脑子里那根"什么时候该用谁"的弦。
1. 搞懂 Grid 之前,先搞清楚它和 Flexbox 的定位差异
1.1 一维与二维:Flexbox 是串串,Grid 是棋盘
很多人一上来就问:有了 Flexbox 为什么还要 Grid?它俩是不是重复了?其实完全不重复,核心差异就在一维和二维这两个词上。
Flexbox 的精髓是"一维布局",它一次只处理一个方向——要么横向排,要么纵向排。主轴上的项目可以灵活伸缩、换行,但副轴方向的排布其实是"顺便"的,你很难用 Flexbox 精确控制"第二行某个项目在第几列"。类比一下,Flexbox 就是一根烤串签子,所有东西都串在这一根轴线上,虽然能串不少,但终究是单线操作。
Grid 则是"二维布局",它同时管理行和列两个方向,数据结构和 Excel 表格一样:有行、有列、有单元格。你在 Grid 里定义一个项目跨几列、跨几行,是从结构上就确定下来的,而不是靠计算宽度或高度"挤"出来的。所以碰到页面骨架、卡片墙、杂志式排版这类"横竖都要管"的场景,Grid 是天然的正确答案。
1.2 什么场景该用 Grid?带着问题看效果
我自己的经验准则很简单:整体骨架用 Grid,内部细节用 Flexbox。页面级布局通常就是 Grid 的主场:后台管理系统的侧边栏+顶栏+内容区,电商网站的商品卡片墙,新闻门户的板块交错排列,这些都需要同时控制行和列,用 Grid 写起来干净得让人感动。
举一个我印象很深的例子。以前用 Flexbox 做商品列表,要保证每行固定 4 列、卡片等宽、最后一行不满也要左对齐,我得写 flex-wrap 加 width 百分比,还得考虑 gap 会不会把宽度撑爆,加上响应式还得改媒体查询。换成 Grid 后,一行grid-template-columns: repeat(auto-fill, minmax(240px, 1fr))就全解决了——列数自动适配、宽度弹性伸缩、换行对齐全部内置。这种体验上的差距,只有亲手写过一遍才懂。
2. 核心概念一次吃透:容器、项目、网格线与六大关键属性
2.1 从一行代码开始:display: grid 与网格容器
Grid 的项目结构跟 Flexbox 类似,分为容器和项目两层。给一个元素加display: grid,它就变成了网格容器,它的直接子元素自动变成网格项目。
.container { display: grid; }光是这一行,你就能看到容器内部发生了变化:子元素不再像普通块级元素那样上下堆叠,而是会按默认规则排成单列多行的网格。不过这只是起点,真正决定网格形态的是接下来要说的行列定义。
网格容器内部有几组核心概念需要先在大脑里建立模型:
- 网格线(Grid Line):划分行列的分界线,纵向的叫列线,横向的叫行线。注意网格线编号从 1 开始,不是 0。
- 网格轨道(Grid Track):两条相邻网格线之间的区域,就是一行或一列。
- 网格单元(Grid Cell):单个行列交叉出的格子,是网格的最小单位。
- 网格区域(Grid Area):由四条网格线围成的矩形区域,可能是多个单元格拼起来的。
这四个概念是你之后阅读任何 Grid 教程的地基。搞懂它们,grid-column、grid-area这些属性就不再是魔法咒语。
2.2 fr 单位、repeat() 和 minmax():灵活刃与硬底线
定义行列最常用的三个工具是fr单位、repeat()函数和minmax()函数。fr是 fraction(份额)的缩写,它表示剩余空间的分配比例,不是固定宽度。这跟 Flexbox 里的flex: 1原理类似,但用起来更直观。
.container { display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 16px; }这里的1fr 2fr 1fr表示三列,第二列占两份,第一、三列各占一份。具体怎么算:假设容器宽度 1000px,gap 是 16px 且有两个间隙,总共是 32px,剩余空间就是 1000 - 32 = 968px。一份是 968 ÷ 4 = 242px,所以三列分别是 242px、484px、242px。注意计算顺序是先扣除 gap,再按比例分配,这个顺序在设置百分比列宽时特别容易踩坑。
repeat()是用来简化重复写法的。repeat(3, 1fr)等价于1fr 1fr 1fr,它还能配合其他写法混用,比如grid-template-columns: 200px repeat(2, 1fr)表示第一列固定 200px,后两列平分剩余空间。
minmax()则是给轨道设定一个范围,常见搭配是minmax(200px, 1fr),意思是列宽最小 200px,有额外空间时按 1fr 扩大。这个函数在响应式布局里是香饽饽,后面做卡片墙案例时会再见到。
2.3 网格线定位:让项目想跨几列就跨几列
定义好网格轨道之后,默认情况下每个子元素会按顺序自动填进一个网格单元里。但实际版面往往需要某个区域占两列、某篇文章占两行,这时候就要用网格线的定位属性。
.item-feature { grid-column: 1 / 3; grid-row: 1 / 3; }grid-column: 1 / 3表示这个项目从第 1 条列线开始,到第 3 条列线结束,也就是跨越了第 1 列和第 2 列。grid-row: 1 / 3同理,跨两行。这里的编号判断方式我总结了一句口诀:写的是网格线编号,不是列数编号。你要跨两列,终点线的编号是起始线编号加 2,不是加 1,新手最容易在这里数错。
如果嫌计算终点麻烦,可以用span关键字,grid-column: 1 / span 2表示从第 1 条列线开始,横跨 2 列。语义上更接近"这个元素要占多大",可读性也更好,我自己更推荐这种写法。
2.4 grid-template-areas:用"画图"替代数线
网格线定位用多了你会发现,复杂布局里数编号特别容易头晕。Grid 为此给了一个直观到离谱的写法:grid-template-areas。它允许你用 ASCII 图形来定义页面骨架,比如一个经典的后台布局:
.layout { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: 60px 1fr 40px; grid-template-areas: "header header" "sidebar main" "footer footer"; min-height: 100vh; }我特别喜欢这个特性的原因是,它让布局变成了一幅"地图",每个字符串代表一行,每个单词代表一个单元格区域。你只需要在子元素里写grid-area: header;、grid-area: sidebar;,元素就会自动对号入座。改布局也只需要改这几行字符串,视觉上立刻能看出结构变化,完全不用去算线号。
grid-template-areas有个硬性要求:每个单元格都必须有名字,区域形状必须是矩形。你不能写出一个 L 形的区域,比如第一行有 a b,第二行只有 a,这种定义会直接报错。但它的容错方式很友好,报错信息会提示哪一行没对齐。
3. 对齐与间距:Grid 里容易迷糊的一组属性
3.1 justify-items 和 align-items:管的是"格子里的项目"
Grid 的对齐属性分两组,很多人在这里栽跟头。第一组justify-items和align-items控制的是项目在单元格内部的对齐方式,从容器上设置,所有子项目生效。
justify-items:控制水平方向,可选 start、end、center、stretch。align-items:控制垂直方向,可选 start、end、center、stretch。
默认值是stretch,也就是项目默认会拉伸填满整个单元格。这就是为什么 Grid 里的子元素不设置宽高时,会自动把格子撑满。如果你想做一个内容居中的卡片,可以设置place-items: center,它其实是align-items: center; justify-items: center;的简写。在布局一个按钮、一张图片、一段文字时,这个用法能省掉大量的 margin 凑居中的操作。
3.2 justify-content 和 align-content:管的是"整个网格的位置"
第二组justify-content和align-content控制的是整个网格轨道组在容器里的位置。什么时候会生效?当网格总宽度小于容器宽度,或者总高度小于容器高度时,才会看出区别。
举个实际例子:容器宽 1000px,你定义了三列固定宽度 200px,网格总宽度只有 600px,剩下 400px 不知道去哪了。此时justify-content: center会把这三列整体居中,justify-content: space-between会让三列贴左、右两端,中间一列居中,这种分布方式和 Flexbox 的justify-content效果一致。
搞混这两组属性的后果很典型:你想把内容居中,结果写了justify-content: center发现毫无变化,因为项目本身已经默认填满格子了。先判断"是放在单元格里"还是"整个网格放在容器里",再选对应的属性组,就不会踩坑。
3.3 gap:间距的正确打开方式
Grid 的间距属性gap是我用回不去的功能之一。它代替了以前 Flexbox 里靠 margin 加负 margin 甚至 nth-child 选择的野路子。gap可以同时设置行距和列距:
.container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px 24px; }这里16px是行间距,24px是列间距。只写一个值则两个方向相同。gap不会像 margin 那样把项目向外推导致容器溢出,也不会让首尾元素多出额外间距,它只负责项目之间的空隙。按理说一个这么合理的属性应该早就普及了,但确实还有不少老项目还在用 margin 方案,如果你维护这类代码,可以找机会顺手升级一下。
4. 实战三步走:三个高频布局场景完整拆解
4.1 场景一:经典后台管理系统骨架
后台管理界面几乎是 Grid 布局的标准教材。它需要固定的侧边栏、自适应宽度的主体、顶栏和底部栏,结构非常规整。用 Flexbox 做这个需要嵌套好几层容器,而用 Grid 只需要一个容器加grid-template-areas。
<div class="layout"> <header class="header">顶部导航</header> <aside class="sidebar">侧边菜单</aside> <main class="main">主要内容区</main> <footer class="footer">底部版权</footer> </div>.layout { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: 60px 1fr 40px; grid-template-areas: "header header" "sidebar main" "footer footer"; min-height: 100vh; } .header { grid-area: header; } .sidebar { grid-area: sidebar; } .main { grid-area: main; } .footer { grid-area: footer; }min-height: 100vh保证了页面骨架至少撑满整个视口高度,即使内容不够,footer 也保持在底部。这个骨架最爽的地方在于响应式适配:在移动端只要在媒体查询里改几行区域字符串,布局立刻从左右结构变成上下结构:
@media (max-width: 768px) { .layout { grid-template-columns: 1fr; grid-template-areas: "header" "main" "sidebar" "footer"; } }侧边栏在移动端被挪到了主内容后面,视觉上先看内容再看菜单,这种调整在旧方案里往往要改 HTML 结构或大动 CSS,在 Grid 里就是改几行字符串的事。
4.2 场景二:自适应商品卡片墙
商品列表、图片库、博客卡片这类"数量不确定、每行都想自动适配"的网格场景,用repeat(auto-fill, minmax(...))是最佳解法。
.card-wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }这一行的意思是:自动填充列,每一列最小宽度 240px,有剩余空间时按 1fr 分配。容器是 1000px 宽时,能排下 4 列(240×4 + gap×3 = 1008px,实际计算时浏览器会智能调整),容器到 800px 时自动变成 3 列,到 500px 时变成 2 列。整个过程不需要一个媒体查询。
这里要注意auto-fill和auto-fit的区别,这俩看起来差不多,行为却完全不同。auto-fill会保留空轨道,即使项目不够多,空白列仍然占位;auto-fit则会把空轨道折叠掉,让已有项目拉伸填满整行。单行情况下,auto-fit能让一两个项目铺满整行更美观,多行商品墙则用auto-fill更符合预期。你可以自己开个 DevTools 拖动容器宽度,用同屏对比的方式感受差别。
4.3 场景三:杂志风格交错排版
前面两个场景的网格都是规规矩矩对齐的,第三个场景来点"不规则"。杂志排版常见做法是头条大图占据两列两行,其他小卡片正常铺排,偶尔穿插一些横向跨度不同的模块。
<div class="news-grid"> <article class="card feature">头条大图</article> <article class="card">普通卡片 1</article> <article class="card">普通卡片 2</article> <article class="card wide">横向宽卡</article> <article class="card">普通卡片 3</article> </div>.news-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 120px; gap: 12px; } .feature { grid-column: span 2; grid-row: span 2; } .wide { grid-column: span 2; }grid-auto-rows: 120px很关键。因为这里没有用grid-template-rows定义行高,而是允许浏览器自动生成行,如果不设定固定行高,所有行都会被内容撑成不同高度,交错效果就会乱掉。给一个固定 120px 的基础行高,配合span 2的大卡片占 240px 高,网格看起来才会有整齐中的变化感。
如果卡片顺序导致网格里出现空洞,比如某个跨两列的元素把后续项目挤到下一行,留下一个空格子,可以给容器加grid-auto-flow: dense让后续项目回填空位。但要注意dense会改变项目的视觉顺序,让 DOM 顺序和视觉顺序不一致,屏幕阅读器和键盘导航也会跟着乱,如果是靠前的位置有重要内容,不建议无脑开启。
5. 避坑指南:Grid 实战中高频出现的问题与排查
5.1 自动生成的行列尺寸失控
Grid 有个特性是隐式网格。当项目数量超出你显式定义的行列轨道时,Grid 会自动创建新的轨道来放这些项目。问题是,隐式轨道的默认尺寸是auto,也就是由内容撑开。比如你定义了grid-template-columns: repeat(3, 1fr)放 5 个项目,前 3 个在第一行,后 2 个被系统自动创建了一个新行,这个新行的高度由第 4、5 个项目的内容高度决定,很可能比第一行矮或高,导致卡片不等高。
解决办法是显式设置隐式轨道尺寸:grid-auto-rows: 1fr或固定值。这样不管项目多少、内容多少,所有自动生成的行都会保持统一高度。这个属性我几乎每写一个卡片墙都会带上,属于预防性写法。
5.2 fr 和百分比混用时的宽度账
fr是在扣除固定尺寸和 gap 之后分配剩余空间的,如果你混用了固定列宽200px、百分比列宽30%和1fr,计算顺序是先处理固定和百分比,再让 fr 分剩下的。有一个很容易被忽视的细节:百分比的基准是容器宽度,但 Grid 项目默认不包含border-box之外的间距预算,所以30%是实打实按容器宽度算的,而不是按"扣除 gap 后"算的。
如果页面出现横向滚动条或者列宽分配和预期不符,先检查是不是百分比列宽加 gap 把总宽度撑超了。我的习惯是尽量统一用fr和minmax,除非列宽真的必须严格按容器比例,否则避免在 Grid 里混用百分比。
5.3 项目内部再布局:别把 Grid 塞到最深层
Grid 虽然能处理二维布局,但它的定位是"布局框架"。如果一个卡片内部只有一行文字加一个按钮,这种内部小结构再去定义 Grid 就有点大材小用了,用 Flexbox 或 block 布局更轻快。
网格项目的子元素不会自动变成网格项目,网格只作用于容器的直接子元素这一层。你如果发现写了display: grid但内部某些嵌套元素没有按预期排列,多半是把 Grid 写在了错误的层级。记住:Grid 管一层,Flexbox 管一层,两者可以嵌套。这也是我推荐"页面骨架 Grid + 模块内部 Flexbox"组合拳的原因。
5.4 问题排查速查表
| 常见现象 | 可能原因 | 解决办法 |
|---|---|---|
| 项目没有按行列排列 | 忘了在容器上写display: grid | 确认容器元素设置了display: grid |
| 列数不是预期数量 | repeat参数写错或auto-fill/auto-fit用反 | 检查grid-template-columns定义,单行效果优先选auto-fit |
| 卡片高度不一致 | 隐式行高默认auto | 加grid-auto-rows: 1fr或固定高度 |
| 网格线跨行跨列没生效 | 终点线编号数错;用了 0 作为起始编号 | 确认编号从 1 开始;改用span语法 |
justify-content没反应 | 网格总宽度正好等于容器宽度 | 先让网格总宽度小于容器宽度再测试 |
| gap 不生效 | 用了旧版grid-gap但浏览器版本旧 | 现代浏览器直接写gap,兼容旧环境保留grid-gap声明 |
| 视觉顺序和 DOM 顺序不一致 | 开启了grid-auto-flow: dense | 确认对无障碍可接受,或者关闭 dense 改用显式定位 |
5.5 Grid 和 Flexbox 到底怎么选
最后的选型问题,我用一句话总结:内容在一条线上流动用 Flexbox,整体骨架在二维平面分配用 Grid。细节一点说,导航栏、按钮组、表单横向排列这些"一行东西"永远属于 Flexbox;页面整体布局、卡片墙、需要同时保证行和列对齐的场景,属于 Grid。二者不是竞争关系,是互补关系,好的页面通常两种都在用。
兼容性方面,现代浏览器对 Grid 的支持已经非常成熟,移动端和桌面端的 Chrome、Firefox、Safari、Edge 都稳定支持,日常业务可以放心用。如果非要照顾上古浏览器,Grid 的属性本身支持@supports做降级判断,给不支持的环境准备一套 Flexbox 兜底布局,但说实话,这类场景在现在的项目里已经越来越少了。
我个人在使用 Grid 之后最大的变化是:写页面骨架的焦虑感没了。以前那些"左侧固定右侧自适应""两边定宽中间弹性"的圣杯需求,现在都是几行声明的事。从第一次用grid-template-areas画完整个后台布局到现在,我已经很少再为一个版面结构纠结超过五分钟。如果你还在用老方案硬撑复杂排版,找个小页面用 Grid 重构一遍试试,那种"这也能一行搞定"的爽快感,很容易上瘾。