CSS Grid 真正好用的地方:把二维布局写清楚

选 Grid 还是 Flex,我现在只看一件事:这块布局是不是天然带着“行和列”两个维度。

一排按钮横向排开,只有一个方向要对齐,Flex 更省事;一个后台仪表盘,左侧筛选栏固定宽、右侧图表区自适应、下面还压一排统计卡片,行和列都得管,这就是 Grid 的主场。判断标准不是“我想练哪个 API”,而是这块结构里有没有需要同时对齐的两个轴。有,用 Grid;只有一个轴,用 Flex。

我把这条判断标准列得很具体:

  • 一行内或一列内的对齐——Flex
  • 行和列要同时控制——Grid
  • 页面级、区域级的骨架——Grid
  • 小组件内部的图标与文字、按钮组——Flex

两者根本不是竞争关系。真实项目里最常见的形态,是外层用 Grid 搭结构、内层用 Flex 做局部对齐。所以别为了证明自己掌握 Grid,把每一处局部对齐都硬写成网格;也别因为 Flex 顺手,就用一堆百分比加媒体查询去硬凑本该是二维的骨架。

我真正开始频繁用 Grid,就是被那个仪表盘逼出来的。早期版本全用 Flex 加百分比宽度加媒体查询,筛选栏、图表区、卡片行相互挤,改一个断点带崩另一处,越写越绕。换成 Grid 之后骨架一下清爽了——它的价值不是“能排成网格”,而是能把二维结构直接写进 CSS,而不是藏在一串宽度计算里。

一个基础网格,以及怎么确认它算对了

容器上 display: grid,定义列和间距:

1.cards {
2  display: grid;
3  grid-template-columns: repeat(3, 1fr);
4  gap: 20px;
5}

三列等宽。1fr 是“占一份剩余空间”,repeat(3, 1fr) 是重复三次的简写。

这里有个值得动手验证的点:grid-template-columns 写的是 repeat(3, 1fr),但浏览器算完会换算成具体像素。假设容器内容宽 660px、gap: 20px,扣掉两道 20px 的缝,剩 620px 三等分,每列约 206.667px。在控制台读 getComputedStyle(document.querySelector('.cards')).gridTemplateColumns,返回的不是 "repeat(3, 1fr)",而是算好的 "206.667px 206.667px 206.667px"——想确认列宽分配对不对,这是最快的办法。更直观的是 DevTools 的 Elements 面板:grid 容器旁会出现一个 grid 徽章,点开能在页面上叠加行线、列线和每条轨道的尺寸标注,gap 的真实间距也标得清清楚楚。

顺带说间距写法。现在统一用 gap,而不是旧的 grid-gap。旧写法还能跑,但 gap 对 Grid 和 Flex 都适用,语义更一致——这也是 Grid 和 Flex 越来越能配合的一个信号。

用 minmax 做响应式,断点由内容定

固定三列在桌面端没问题,移动端就挤成一团。更实用的是 repeat + minmax

1.cards {
2  display: grid;
3  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
4  gap: 20px;
5}

含义是:每张卡片最小 240px,空间够就自动加列,空间不够就自动换行。文章卡片、商品卡片、相册列表用这套,很多时候一行媒体查询都不用写,就能得到自然的响应式。

minmax(240px, 1fr) 里那个最小值不能随手填。它其实就是断点,得看卡片里塞了什么撑得住:一张只有标题和价格的卡片,240px 够了;换成带图表的复杂卡片,最小宽度可能要 320px。断点由内容撑不撑得住决定,不是拍脑袋。

auto-fit 和 auto-fill,眼见为实

这两个很像,但行为不同。auto-fill 会尽量创建可用列,哪怕某些列是空的;auto-fit 会把空列折叠掉,让已有项目占满空间。

大多数卡片列表想要的是 auto-fit

1grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));

只有当你要保留空轨道占位(比如固定网格),才更可能用 auto-fill

差别能直接看出来。拿一个 width: 800px 的容器,只放 2 个子项,列都用 minmax(200px, 1fr)auto-fill 时容器算得下 4 列(800/200),多出来的 2 条空轨道保留,于是两个子项各停在 200px、右边空一大片;换成 auto-fit,空轨道被折叠成 0 宽,两个子项平分 800px、各撑到 400px。读 getComputedStyle(el).gridTemplateColumns 也验证得了——auto-fill 会返回四段尺寸(含空轨道),auto-fit 那两段空轨道是 0px

区域命名让页面结构自解释

grid-template-areas 能给每块区域起名字,直接把布局画出来:

1.layout {
2  display: grid;
3  grid-template-columns: 240px 1fr;
4  grid-template-rows: auto 1fr auto;
5  grid-template-areas:
6    "header header"
7    "sidebar main"
8    "footer footer";
9  min-height: 100vh;
10}
11
12.header { grid-area: header; }
13.sidebar { grid-area: sidebar; }
14.main { grid-area: main; }
15.footer { grid-area: footer; }

HTML 保持干净:

1<div class="layout">
2  <header class="header">Header</header>
3  <aside class="sidebar">Sidebar</aside>
4  <main class="main">Main Content</main>
5  <footer class="footer">Footer</footer>
6</div>

后台系统、文档站、博客详情页特别适合这种写法。看一眼 CSS 就知道整体骨架,不用靠一堆嵌套 div 反推结构。

响应式重排也顺理成章——不改 HTML,只调 areas:

1@media (max-width: 768px) {
2  .layout {
3    grid-template-columns: 1fr;
4    grid-template-areas:
5      "header"
6      "main"
7      "sidebar"
8      "footer";
9  }
10}

桌面端侧栏在左,移动端挪到正文之后,全靠 CSS 完成。

但这里有个判断边界要守住:视觉顺序能重排,语义顺序不能乱。 CSS 改的是视觉位置,键盘焦点和屏幕阅读器仍按 DOM 顺序走。移动端把侧栏视觉上放到正文下面没问题,可如果 DOM 里侧栏在正文之前,键盘用户还是得先趟过一整片导航才够得着正文。布局可以重排,内容的语义顺序要先想好、别为了视觉迁就而把重要内容塞到 DOM 里不合理的位置。

跨列跨行,交给 grid-column / grid-row

Grid 里让元素跨多行多列很自然:

1.featured-card {
2  grid-column: span 2;
3  grid-row: span 2;
4}

重点卡片占更大区域,资讯首页、作品集、数据看板里到处是这种。也可以明确指定起止线:

1.hero {
2  grid-column: 1 / -1;
3}

1 / -1 是从第一条网格线跨到最后一条,让标题、横幅、分组头占满整行。这类“跨整行”的表达,Flex 做起来就别扭得多——又一个二维需求该用 Grid 的例子。

对齐:容器和项目要分清

Grid 的对齐属性有两组,最容易混。控制每个网格项在自己格子里的位置:

1.grid {
2  justify-items: center;
3  align-items: center;
4}

控制整张网格在容器里的位置:

1.grid {
2  justify-content: center;
3  align-content: start;
4}

记法很简单:items 管项目,content 管整张网格。单个项目要单独调,用 justify-self / align-self

内容溢出:1fr 不收缩,先查 min-width

Grid 里最常见的坑,是长内容把列撑破。卡片标题是一串没有空格的长英文或链接,就可能把 1fr 顶开,页面横向出滚动条。

给网格项补 min-width: 0

1.card {
2  min-width: 0;
3}
4
5.card-title {
6  overflow: hidden;
7  text-overflow: ellipsis;
8  white-space: nowrap;
9}

后台表格、卡片列表、两列布局里这坑反复出现。只要发现 1fr 不按预期收缩,第一反应就该是检查子元素是不是需要 min-width: 0 原因是 grid item 的 min-width 默认是 auto,也就是“至少要容得下内容的最小尺寸”,内容不肯缩,1fr 就缩不动。我在一个左固定右 1fr 的两列布局里被这个坑了好几次,右侧表格死活把页面撑出横向滚动,最后给右侧容器补一句 min-width: 0 就正常了。

显式轨道与隐式轨道,别把行数写死

有个判断经常被忽略:你到底要不要把行数写死。grid-template-rows 定义的是显式轨道,而当项目数量超过你声明的轨道时,浏览器会自动生成隐式轨道。隐式轨道默认是 auto 高度,往往不是你想要的。

比如卡片数量不固定,就别去数“该有几行”,交给隐式行,再用 grid-auto-rows 统一它们的尺寸:

1.cards {
2  display: grid;
3  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
4  grid-auto-rows: minmax(160px, auto);
5  gap: 20px;
6}

这样列数随宽度自适应、行数随内容自增,每行最矮 160px、内容多了自己长高。判断依据是:行数是否由数据条数决定?是,就别写死 grid-template-rows,把它留给隐式轨道。

隐式生成的方向也能控。默认沿行铺(grid-auto-flow: row),想让项目优先填满列再换行就改成 column。还有个很实用的值 dense,它会回填前面因为跨列留下的空洞:

1.masonry-like {
2  grid-auto-flow: dense;
3}

瀑布流式的图墙里,某张图 grid-column: span 2 撑大后前排容易留缝,dense 会把后面的小图补进去。代价是视觉顺序可能和 DOM 顺序脱节,涉及键盘 Tab 顺序时要留意——又回到前面那条“视觉能重排、语义别乱”的边界。

subgrid:嵌套对齐的解法,但今年 Chrome 还得等

还有一类需求靠普通 Grid 很别扭:卡片列表里,每张卡内部的标题、正文、按钮想跨卡对齐,让所有卡的按钮在同一条基线上。子网格各算各的高度,对不齐。

subgrid 就是为这个来的——让子项目复用父网格的轨道线,而不是自己新开一套:

1.card {
2  display: grid;
3  grid-template-rows: subgrid;
4  grid-row: span 3;
5}

判断值不值得上它,今年得把兼容性算进去。Safari 从 16 起、Firefox 早就支持,但 Chrome 目前还没跟上,还在往后排。所以我现在只在内部、能锁定浏览器的工具里试 subgrid,对外的电商页面仍老老实实用固定行高或 Flex 兜底对齐。用之前照例 @supports (grid-template-rows: subgrid) 探一下,不支持就退回普通网格,别让对齐效果在 Chrome 用户那儿直接崩掉。

Grid 和 Flex 配合,各管一层

回到最初那条判断标准,落到代码上通常是这样:

1.dashboard {
2  display: grid;
3  grid-template-columns: 260px 1fr;
4  gap: 24px;
5}
6
7.toolbar {
8  display: flex;
9  align-items: center;
10  justify-content: space-between;
11  gap: 12px;
12}

外层仪表盘用 Grid 分侧栏和内容区,工具栏内部用 Flex 排标题、筛选器和按钮。各管一层,比强行只用一种自然得多。

今年容器查询也让这套组合更好用了。过去卡片布局只能看 viewport 宽度,现在能按组件容器自身的宽度决定列数——同一个组件放在主内容区排三列、放进侧栏排一列。这种场景用容器查询比全局媒体查询贴切太多:

1.card-section {
2  container-type: inline-size;
3}
4
5@container (min-width: 720px) {
6  .card-section__grid {
7    grid-template-columns: repeat(3, 1fr);
8  }
9}

容器查询是去年下半年才在主流浏览器落地的,今年用起来底气足了不少,但我还是会给不支持的浏览器留一套默认单列样式兜底,不把它当成理所当然。

判断要不要上 Grid,我基本不问“能不能排出来”——Flex 硬凑几乎什么都能排出来。我问的是这块布局是不是天然有行有列。页面骨架、仪表盘、卡片墙、复杂表单,多半有;按钮组、标题栏、图标文字对齐,多半没有。frminmaxauto-fit、区域命名、跨行跨列、min-width: 0 这些细节吃透之后,很多原本靠嵌套和媒体查询硬撑的布局,会一下子变得直观。Grid 最舒服的地方,就是让二维结构直接出现在 CSS 里。