CSS Grid 交互式指南(译)(上)

333 阅读11分钟

📃 原文地址: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 来定义的

image.png

image.png

使用 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 是如何自动组织这些子元素的。

image.png

当我们没有显式定义行时,CSS Grid 会自动创建“隐式网格” 。每个子元素默认都会被放置在新的一行中 —— 也就是说,每个子元素会占据一整行。随着子元素的增加,Grid 会自动添加或移除行。

这是一个非常“动态”的机制,不需要你手动添加任何行结构。

默认情况下,Grid 容器的高度是由它的子元素决定的 —— 它会自动根据子元素的数量增长或收缩。这其实并不特属于 CSS Grid,而是 CSS 的通用行为:

Grid 容器本身仍然受 普通文档流(Flow Layout) 的影响,属于块级元素(block element),因此它会垂直扩展以包裹住所有子元素。

也就是说,虽然我们在用 Grid 管理子元素的布局,但 Grid 容器本身依然是遵循文档流规则来确定尺寸的。

那如果我们给容器一个固定高度呢?假设我们给 Grid 容器设定了一个固定高度,比如 height: 300px。此时 Grid 的表现会发生改变:

  • 它会根据已有的行数和内容,把这片固定的“画布”划分成等高的格子
  • 所有的子元素就会被限制在这个高度范围内,布局变得更加精确可控。

这也是 CSS Grid 与其他布局模式最大的不同之一:

你既可以让它自动扩展,也可以完全控制它的网格结构。

image.png

image.png

🏗️ 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。是不是很像在做切蛋糕?

image.png

使用 fr,你可以获得类似 Flexbox 的弹性布局效果。 它与百分比 % 或固定单位(如 pxrem)最大的不同在于:

  • %px硬性约束,会强制列宽;
  • fr软性弹性单位,它会自动伸缩,去适应内容和剩余空间。

也就是说:

当你希望布局能够根据容器变化而动态调整时,fr 是你的首选!

📉 试试看:改变容器宽度感受一下差异!(注:请跳转到原文查看代码效果,以下只是截图)

image.png

假设我们的第一列中放了一个可爱的幽灵图片(如图所示),这个图片有一个明确的宽度:55px。那么问题来了:

如果列宽不够容纳这只幽灵会怎样?

如果我们用的是 % 来定义列宽:

grid-template-columns: 30% 70%;

这时,列宽是刚性的。如果第一列的宽度小于图片的宽度(55px),那就会发生内容溢出 —— 图片会“撑破”格子,越界显示。

如果我们使用 fr 来定义列宽:

grid-template-columns: 1fr 2fr;

这种情况下,Grid 会变得“聪明”很多:

  • 它不会让列缩得太小;
  • 它会根据内容的最小宽度,自动调整列的尺寸;
  • 即便这会打破原有的 1:2 比例,也会优先保证内容完整显示

这种行为非常类似于 flex-grow 的机制,在我写的《Flexbox 交互式指南》中也有详细介绍。fr 单位并不直接决定列宽,而是这样工作的:

  1. 先计算所有列的最小宽度(以内容为准);
  2. 再将剩余空间(如果有)fr 值的比例进行分配

所以如果内容本身就比较宽,fr 也不会硬性压缩它。


gap 是 CSS Grid 中非常神奇的属性,它可以自动在所有行和列之间添加间距,不用你再手动写 margin

来看看当我们在百分比(%)和比例单位(fr)之间切换时,会发生什么:

image.png

image.png

你注意到在使用百分比列宽时,内容会溢出到网格容器外面了吗?这是因为百分比的计算方式是基于整个网格容器的宽度。在例子中,两列分别占用了 25%75%,正好是 100%刚好填满了容器的全部空间。然而,当我们设置了 16pxgap 后,这些固定的列宽并不会自动收缩来给间距让出空间,于是就只能撑破容器

与此不同,fr 单位是基于剩余空间来计算的。比如当容器设置了 gap: 16px,那么总空间会先减掉这个间距的值,然后剩下的空间再按比例(如 1fr + 3fr)进行分配。这使得布局在视觉上更加灵活,也更符合我们的预期

总之:

  • %固定的、刚性的,不会为 gap 让路;
  • fr自适应的、弹性的,会自动考虑 gap 的影响。

🔄 gapgrid-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 个子元素,会发生什么?

我们一起来看看。

image.png 有趣的是,当我们在一个两列的网格中放入 3 个子元素时,网格自动生成了第二行

这其实是 CSS Grid 的内置算法行为

它的目标是确保每个子元素都有一个单独的网格单元格(grid cell)。
如果列数不够,它就会按需生成新的行来容纳剩余的子元素。

这种行为在一些场景下非常方便,比如我们有一个不确定数量的元素(如照片墙、视频列表等),就不需要提前写死多少行,网格会自动帮我们扩展。

当然,在某些情况下我们可能希望更精确地控制布局结构,比如:

  • 首页布局需要固定几行几列的模块
  • 面板区域需要指定某些行的高度

这时,我们就可以使用 grid-template-rows 属性显式地定义行数和行高

例如:

image.png

当我们同时定义了 grid-template-rowsgrid-template-columns,就创建了一个显式网格(Explicit Grid)。

这意味着:

  • 每一行、每一列的大小都是我们手动定义的;
  • 网格结构是完全可控、固定的。

这种方式非常适合用于构建页面级布局,比如教程开头提到的经典 Holy Grail 布局(圣杯布局)。

🧮 重复真方便!repeat 助手函数

假设我们现在要做一个日历视图

image.png 你可能需要这样定义:

.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,展示了完整的代码示例(以下只是截图,想看源码请跳转到原网址):

image.png

📅 日历与可访问性(Calendars and accessibility)

这个日历只是一个快速演示,展示我们如何使用 CSS Grid 来构建特定布局的简单示例。它并不适用于生产环境。

如果你打算构建一个日期选择器(date-picker),强烈建议你使用专注于可访问性(accessibility)的专业库,比如 React Aria

(未完待续,下篇正在翻译整理中)