📃 原文地址:An Interactive Guide to CSS Grid
👨💼 原文作者: Joshw Comeau
📂 归类:CSS
🗓️ 初次发布:2023 年 11 月 21 日
🔄 最近更新:2025 年 5 月 9 日
CSS Grid:CSS 中最神奇的部分之一 ✨
CSS Grid 是 CSS 语言中最令人惊艳的部分之一。它为我们提供了一整套全新的工具,让我们可以创建出复杂而灵活的网页布局。
不过,它也出奇地复杂。我自己也花了不少时间,才真正掌握 CSS Grid 的使用方法!
在这篇教程中,我会分享我学习 CSS Grid 过程中的一些 💡“灵光一现”的关键时刻。你将了解这种布局模式的基础知识,并学习如何用它做出一些非常酷的效果。✨
浏览器兼容性?
虽然 CSS Grid 是目前最现代的布局工具,但它其实并不算是非常新潮的“前沿技术” —— 自 2017 年以来,所有主流浏览器就已经支持它了!
根据 Can I Use 的数据,CSS Grid 的全球用户支持率约为 97.8% 。这已经是非常优秀的兼容性了;就连 Flexbox 的支持率也只是高了约 0.5% 而已!
🧠 心智模型:你真的理解了 Grid 吗?
CSS 其实由多个不同的布局算法组成,每种算法都是为不同类型的界面而设计的:
- 默认的 Flow Layout(文档流布局)适合数字文档;
- Table Layout(表格布局)适合结构化的表格数据;
- Flexbox 则适合在单一方向上分配元素。
而 CSS Grid,是最新也是最强大的布局算法。它拥有极高的灵活性:我们可以用它来构建复杂的布局,并能根据不同条件动态适应变化。
在我看来,Grid 最不寻常的一点在于:
网格的结构 —— 也就是行与列 —— 是完全由 CSS 来定义的。
使用 CSS Grid 时,我们可以将一个单一的 DOM 元素容器划分成多行多列。就像把它切成一个个格子一样。在本教程中,我们会用虚线来展示这些行列(为教学需要),但在实际网页中,这些网格线是不可见的。
这听起来是不是有点奇怪?因为在其他任何布局模式中,要想创建这种“分隔格子”的效果,通常都得新增一堆 DOM 元素才行。
比如在传统的 表格布局(Table Layout) 中:
- 每一行需要用
<tr>标签来表示; - 每个单元格需要用
<td>或<th>来表示; - 结构层级非常明确,必须在 HTML 中“写死”。
<table>
<tbody>
<!-- First row -->
<tr>
<!-- Cells in the first row -->
<td></td>
<td></td>
<td></td>
</tr>
<!-- Second row -->
<tr>
<!-- Cells in the second row -->
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
而在 CSS Grid 中,这一切就不一样了。
CSS Grid 让我们完全通过 CSS 来管理布局结构。我们可以自由地“切割”容器,定义我们需要的网格划分。然后,Grid 子元素就可以基于这些格子进行对齐、定位与布局。
也就是说:
你不再需要在 HTML 中写一堆结构,只需要用 CSS 就能控制整个布局!
这就是 CSS Grid 最与众不同、也最强大的地方之一。
🌀 网格的流动(Grid Flow)
我们只需要在容器元素上添加 display: grid,就能启用 Grid 布局模式:
.wrapper {
display: grid;
}
启用后,默认情况下,Grid 会创建一列,并根据子元素的数量自动创建多行。因为我们没有显式地定义行列结构,所以这称为:隐式网格(implicit grid) 。
通俗来说就是:
你什么都没定义,但 Grid 会聪明地根据内容自动填满格子。
接下来,我们将看到 Grid 是如何自动组织这些子元素的。
当我们没有显式定义行时,CSS Grid 会自动创建“隐式网格” 。每个子元素默认都会被放置在新的一行中 —— 也就是说,每个子元素会占据一整行。随着子元素的增加,Grid 会自动添加或移除行。
这是一个非常“动态”的机制,不需要你手动添加任何行结构。
默认情况下,Grid 容器的高度是由它的子元素决定的 —— 它会自动根据子元素的数量增长或收缩。这其实并不特属于 CSS Grid,而是 CSS 的通用行为:
Grid 容器本身仍然受 普通文档流(Flow Layout) 的影响,属于块级元素(block element),因此它会垂直扩展以包裹住所有子元素。
也就是说,虽然我们在用 Grid 管理子元素的布局,但 Grid 容器本身依然是遵循文档流规则来确定尺寸的。
那如果我们给容器一个固定高度呢?假设我们给 Grid 容器设定了一个固定高度,比如 height: 300px。此时 Grid 的表现会发生改变:
- 它会根据已有的行数和内容,把这片固定的“画布”划分成等高的格子;
- 所有的子元素就会被限制在这个高度范围内,布局变得更加精确可控。
这也是 CSS Grid 与其他布局模式最大的不同之一:
你既可以让它自动扩展,也可以完全控制它的网格结构。
🏗️ Grid 的构建(Grid Construction)
如前面所说,CSS Grid 默认会创建一个单列布局。但我们可以使用 grid-template-columns 属性来自定义列的数量和宽度。
🔍 虚线只是示意,不存在于真实页面中
为了帮助大家看清楚列和行的划分,我用了“虚线”来显示网格边界。但实际上: CSS Grid 的列线 / 行线本质是不可见的,不能用 CSS 直接“画”出来。
这些虚线只是我用了 ✨一点博客魔法✨ —— 通过伪元素(
::before、::after)模拟出来的视觉辅助线而已。如果你感兴趣,可以查看 CSS 标签页,看看我是怎么做到的 😉
通过给 grid-template-columns 传入两个值,比如 25% 和 75%,我们就告诉 CSS Grid 算法:
请把这个容器划分为两列:第一列占 25%,第二列占 75%。
这种方式非常直观,是 CSS Grid 中最核心的属性之一。你可以使用任何有效的 CSS <length-percentage> 类型来定义列宽,比如:
px(像素)rem(相对字体大小)%(百分比)vw/vh(视口单位)
除此之外,Grid 还引入了一个全新的单位:
fr—— fraction(比例单位)
它的意思是:“这个元素应该占据剩余空间的多少份”。来看一个例子:
grid-template-columns: 1fr 3fr;
这段代码的含义是:
- 第一列占 1 份;
- 第二列占 3 份;
- 总共是 1 + 3 = 4 份;
所以,第一列会占据容器宽度的 1/4,第二列占 3/4。是不是很像在做切蛋糕?
使用 fr,你可以获得类似 Flexbox 的弹性布局效果。
它与百分比 % 或固定单位(如 px、rem)最大的不同在于:
%和px是硬性约束,会强制列宽;fr是软性弹性单位,它会自动伸缩,去适应内容和剩余空间。
也就是说:
当你希望布局能够根据容器变化而动态调整时,
fr是你的首选!
📉 试试看:改变容器宽度感受一下差异!(注:请跳转到原文查看代码效果,以下只是截图)
假设我们的第一列中放了一个可爱的幽灵图片(如图所示),这个图片有一个明确的宽度:55px。那么问题来了:
如果列宽不够容纳这只幽灵会怎样?
如果我们用的是 % 来定义列宽:
grid-template-columns: 30% 70%;
这时,列宽是刚性的。如果第一列的宽度小于图片的宽度(55px),那就会发生内容溢出 —— 图片会“撑破”格子,越界显示。
如果我们使用 fr 来定义列宽:
grid-template-columns: 1fr 2fr;
这种情况下,Grid 会变得“聪明”很多:
- 它不会让列缩得太小;
- 它会根据内容的最小宽度,自动调整列的尺寸;
- 即便这会打破原有的
1:2比例,也会优先保证内容完整显示。
这种行为非常类似于 flex-grow 的机制,在我写的《Flexbox 交互式指南》中也有详细介绍。fr 单位并不直接决定列宽,而是这样工作的:
- 先计算所有列的最小宽度(以内容为准);
- 再将剩余空间(如果有)按
fr值的比例进行分配。
所以如果内容本身就比较宽,fr 也不会硬性压缩它。
gap 是 CSS Grid 中非常神奇的属性,它可以自动在所有行和列之间添加间距,不用你再手动写 margin。
来看看当我们在百分比(%)和比例单位(fr)之间切换时,会发生什么:
你注意到在使用百分比列宽时,内容会溢出到网格容器外面了吗?这是因为百分比的计算方式是基于整个网格容器的宽度。在例子中,两列分别占用了 25% 和 75%,正好是 100%,刚好填满了容器的全部空间。然而,当我们设置了 16px 的 gap 后,这些固定的列宽并不会自动收缩来给间距让出空间,于是就只能撑破容器。
与此不同,fr 单位是基于剩余空间来计算的。比如当容器设置了 gap: 16px,那么总空间会先减掉这个间距的值,然后剩下的空间再按比例(如 1fr + 3fr)进行分配。这使得布局在视觉上更加灵活,也更符合我们的预期。
总之:
%是固定的、刚性的,不会为gap让路;fr是自适应的、弹性的,会自动考虑gap的影响。
🔄
gap与grid-gap的区别最初在 CSS Grid 刚推出时,我们使用
grid-gap属性来为网格的列和行添加间距。但很快,社区就意识到:Flexbox 也需要这个特性!于是,CSS 规范将它通用化为
gap属性,可以同时适用于 Grid 和 Flex 布局。如今:
grid-gap已经被标记为废弃(deprecated);- 所有主流浏览器都已将其等同处理为
gap;- 它们的浏览器兼容性几乎一致,约 96%+ 支持率。
🧠 建议:
无论你是使用 Flexbox 还是 Grid,统一使用
gap更加现代、语义化,也避免语法冗余。 如果项目中已有grid-gap,可以不急于替换,但新代码建议用gap。
🧩 隐式 vs 显式网格行
那接下来一个问题来了:
💬 如果我定义了一个两列的网格(2 columns),但是实际添加了 3 个子元素,会发生什么?
我们一起来看看。
有趣的是,当我们在一个两列的网格中放入 3 个子元素时,网格自动生成了第二行!
这其实是 CSS Grid 的内置算法行为:
它的目标是确保每个子元素都有一个单独的网格单元格(grid cell)。
如果列数不够,它就会按需生成新的行来容纳剩余的子元素。
这种行为在一些场景下非常方便,比如我们有一个不确定数量的元素(如照片墙、视频列表等),就不需要提前写死多少行,网格会自动帮我们扩展。
当然,在某些情况下我们可能希望更精确地控制布局结构,比如:
- 首页布局需要固定几行几列的模块;
- 面板区域需要指定某些行的高度。
这时,我们就可以使用 grid-template-rows 属性显式地定义行数和行高。
例如:
当我们同时定义了 grid-template-rows 和 grid-template-columns,就创建了一个显式网格(Explicit Grid)。
这意味着:
- 每一行、每一列的大小都是我们手动定义的;
- 网格结构是完全可控、固定的。
这种方式非常适合用于构建页面级布局,比如教程开头提到的经典 Holy Grail 布局(圣杯布局)。
🧮 重复真方便!repeat 助手函数
假设我们现在要做一个日历视图
你可能需要这样定义:
.calendar {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
}
但这有点麻烦,尤其是当列数更多(比如 50 列)时,维护成本会很高。
幸好,CSS 提供了一个更优雅的写法 —— repeat():
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
这句代码的意思是:
创建 7 列,每列
1fr宽度。
这样不仅更简洁,也更容易维护,是构建“等宽列布局”的首选方式。 如果你感兴趣,这里有一个 Playground,展示了完整的代码示例(以下只是截图,想看源码请跳转到原网址):
📅 日历与可访问性(Calendars and accessibility)
这个日历只是一个快速演示,展示我们如何使用 CSS Grid 来构建特定布局的简单示例。它并不适用于生产环境。
如果你打算构建一个日期选择器(date-picker),强烈建议你使用专注于可访问性(accessibility)的专业库,比如 React Aria。
(未完待续,下篇正在翻译整理中)