探索 CSS Grid 的隐式网格和自动放置功能

Avatar of Temani Afif
Temani Afif

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

使用 CSS Grid 时,首先需要在要成为网格容器的元素上设置 display: grid。然后,我们使用 grid-template-columnsgrid-template-rowsgrid-template-areas 的组合显式定义网格。接下来,下一步是将项目放置到网格中。

这是应该使用且我推荐的经典方法。但是,还有另一种方法可以创建 **无需任何显式定义** 的网格。我们称之为 **隐式网格**。

“显式、隐式?到底是怎么回事?”

奇怪的术语,对吧?Manuel Matuzovic 已经 什么是 CSS Grid 中的“隐式”和“显式” 做出了很好的解释,但让我们直接深入了解 规范 中所说的内容。

grid-template-rowsgrid-template-columnsgrid-template-areas 属性定义了形成 **显式网格** 的固定数量的轨道。当网格项目位于这些边界之外时,网格容器会通过向网格添加隐式网格线来生成隐式网格轨道。这些线与显式网格一起形成 **隐式网格**。

所以,用通俗易懂的语言来说,如果任何元素恰好被放置在定义的网格外,浏览器会自动生成额外的行和列。

自动放置呢?

与隐式网格的概念类似,自动放置 是浏览器自动将项目放置到网格中的能力。我们并不总是需要指定每个项目的位置。

通过不同的用例,我们将了解这些功能如何帮助我们用几行代码创建复杂且动态的网格。

动态侧边栏

这里,我们有三种不同的布局,但我们只有一个适用于所有布局的网格配置。

main {
  display: grid;
  grid-template-columns: 1fr;
}

只有一列占据了所有可用空间。这是我们的“显式”网格。它被设置为在 main 网格容器中容纳一个网格项目。仅此而已。一列和一行。

但是,如果我们决定在其中放置另一个元素,比如一个 aside(我们的动态侧边栏)呢?根据当前(以及显式)定义,我们的网格将不得不自动调整以找到该元素的位置。如果我们不对 CSS 做任何其他操作,以下是 DevTools 告诉我们正在发生的事情。

该元素占据了容器上显式设置的整列。同时,它落到标记为 2 和 3 的隐式网格线之间的新行上。请注意,我正在使用 20px 间隙来帮助视觉上分隔事物。

我们可以将 <aside> 移动到 <section> 旁边的列。

aside {
  grid-column-start: 2;
}

以下是 DevTools 现在告诉我们的内容。

该元素位于网格容器的第一和第二网格线之间。从第二网格线开始,到我们从未声明的第三条线结束。

我们将元素放置在第二列,但是……我们没有第二列。很奇怪,对吧?我们从未在 <main> 网格容器上声明第二列,但浏览器为我们创建了一个!这是我们查看的规范中的关键部分。

当网格项目位于这些边界之外时,网格容器会通过向网格添加隐式网格线来生成隐式网格轨道。

此强大功能使我们能够拥有动态布局。如果我们只有 <section> 元素,则只会得到一列。但是,如果我们在其中添加 <aside> 元素,则会创建一个额外的列来包含它。

我们可以将 <aside> 放置在 <section> 之前,如下所示。

aside {
  grid-column-end: -2;
} 

这会在网格的开头创建隐式列,这与之前将隐式列放置在末尾的代码不同。

我们可以拥有右侧或左侧侧边栏。

我们可以使用 grid-auto-flow 属性更轻松地完成相同操作,以将任何和所有隐式轨道设置为以 column 方向流动。

现在无需指定 grid-column-start 即可将 <aside> 元素放置在 <section> 的右侧!事实上,我们决定在任何时间放入的任何其他网格项目现在都将以列方向流动,每个项目都放置在其自己的隐式网格轨道中。非常适合在网格中项目数量事先未知的情况!

也就是说,如果我们想将其放置在左侧的列中,我们仍然需要 grid-column-end,因为否则,<aside> 将占据显式列,这反过来会将 <section> 推出显式网格并迫使其占用隐式列。

我知道,我知道。这有点复杂。以下是一个我们可以用来更好地理解这个小怪癖的另一个示例。

在第一个示例中,我们没有指定任何放置。在这种情况下,浏览器将首先将 <aside> 元素放置在显式列中,因为它在 DOM 中排在第一位。同时,<section> 会自动放置在浏览器为我们自动(或隐式)创建的网格列中。

在第二个示例中,我们将 <aside> 元素设置在显式网格外。

aside {
  grid-column-end: -2;
}

现在,<aside> 在 HTML 中是否排在第一位并不重要。通过将 <aside> 重新分配到其他位置,我们使 <section> 元素能够占用显式列。

图像网格

让我们尝试使用图像网格做些不同的事情,其中我们有一个大图像和几个在其旁边(或下方)的缩略图。

我们有两个网格配置。但猜猜怎么了?我根本没有定义任何网格!我所做的就是这个。

.grid img:first-child {
  grid-area: span 3 / span 3;
}

令人惊讶的是,我们只需要一行代码就能实现这样的效果,所以让我们剖析一下发生了什么,您就会发现它比您想象的要容易。首先,grid-area 是一个简写属性,它将以下属性组合到一个声明中。

  • grid-row-start
  • grid-row-end
  • grid-column-start
  • grid-column-end

等等!grid-area 不是我们用来定义 命名区域 而不是元素在网格上开始和结束位置的属性吗?

是的,但它还能做更多的事情。我们可以写很多关于 grid-area 的内容,但在这种特定情况下。

.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* ...is equivalent to: */
.grid img:first-child {
  grid-row-start: span 3;
  grid-column-start: span 3;
  grid-row-end: auto;
  grid-column-end: auto;
}

当我们打开 DevTools 展开简写版本时,可以看到相同的结果。

这意味着网格中的第一个图像元素需要跨越 **三列** 和 **三行**。但由于我们没有定义任何列或行,浏览器会为我们完成此操作。

我们本质上将 HTML 中的第一个图像放置在 3⨉3 网格中。这意味着任何其他图像都将自动放置在相同的三个列中,而无需指定任何新内容。

总而言之,我们告诉浏览器,第一个图像需要占据三个列和三行的空间,这些列和行在我们设置网格容器时从未显式定义过。浏览器为我们设置了这些列和行。因此,**HTML 中其余的图像会直接流入到位,使用相同的三个列和三行**。并且由于第一个图像占据了第一行中的所有三列,因此其余图像会流入包含三个列的其他行中,其中每个图像占据一列。

所有这些都来自一行 CSS!这就是“隐式”网格和自动放置的功能。

对于该演示中的第二个网格配置,我所做的只是使用 grid-auto-flow: column 更改了自动流动方向,就像我们之前在将 <aside> 元素放在 <section> 旁边时所做的那样。这会强制浏览器创建一个它可以用来放置其余图像的 *第四* 列。并且由于我们有三行,因此其余图像会被放置在同一列中。

我们需要向图像添加一些属性以确保它们完美地适应网格而没有任何溢出。

.grid {
  display: grid;
  grid-gap: 10px;
}

/* for the second grid configuration */
.horizontal {
  grid-auto-flow: column;
}

/* The large 3⨉3 image */
.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* Help prevent stretched or distorted images */
img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

当然,我们只需调整一个值,就可以轻松更新网格以考虑更多图像。这个值就是大图像样式中的3。我们现在有这个

.grid img:first-child {
  grid-area: span 3 / span 3;
}

但我们可以简单地将其更改为4,从而添加第四列。

.grid img:first-child {
  grid-area: span 4 / span 4;
}

更好的是:让我们将其设置为自定义属性,以便更容易更新。

动态布局

带有侧边栏的第一个用例是我们的第一个动态布局。现在,我们将处理更复杂的布局,其中元素的数量将决定网格配置。

在这个例子中,我们可以有从一个到四个元素,网格会以一种很好的方式调整以适应元素的数量,而不会留下任何尴尬的间隙或缺失的空间。

当我们有一个元素时,我们什么也不做。该元素会自动扩展以填充网格自动创建的唯一行和列。

但是当我们添加第二个元素时,我们使用grid-column-start: 2创建另一列(隐式)。

当我们添加第三个元素时,它应该占据两列的宽度——这就是我们使用grid-column-start: span 2的原因,但前提是它是:last-child,因为如果(以及何时)我们添加第四个元素,那个元素应该只占据一列。

加起来,我们只有两个声明和隐式网格的魔力,就有了四种网格配置

.grid {
  display: grid;
}
.grid :nth-child(2) {
  grid-column-start: 2;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}

让我们再试一个

对于只有 1 个或 2 个元素的第一种和第二种情况,我们什么也不做。但是,当我们添加第三个元素时,我们会告诉浏览器——只要它是:last-child——它应该跨越两列。当我们添加第四个元素时,我们告诉浏览器该元素需要放置在第二列。

.grid {
  display: grid;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}
.grid :nth-child(4) {
  grid-column-start: 2;
}

你开始掌握技巧了吗?我们根据元素的数量(使用:nth-child)向浏览器提供具体的指令,并且有时一条指令可以完全改变布局。

需要注意的是,当我们使用不同内容时,大小不会相同。

由于我们没有为项目定义任何大小,浏览器会根据其内容自动为我们调整大小,我们最终得到的大小可能与我们刚刚看到的不同。为了克服这个问题,我们必须明确指定所有列和行的大小相同

grid-auto-rows: 1fr;
grid-auto-columns: 1fr;

嘿,我们还没有使用过这些属性!grid-auto-rowsgrid-auto-columns 分别设置网格容器中隐式行和列的大小。或者,正如规范解释的那样

grid-auto-columnsgrid-auto-rows 属性指定未由grid-template-rowsgrid-template-columns 指定大小的轨道的大小。

这是另一个示例,我们可以最多使用六个元素。这次我让你来剖析代码。别担心,选择器可能看起来很复杂,但逻辑非常简单。

即使有六个元素,我们也只需要两个声明。想象一下,我们只需几行代码就能实现所有复杂和动态的布局!

grid-auto-rows 发生了什么,为什么它需要三个值?我们定义了三行吗?

不,我们没有定义三行。但我们确实定义了三个值作为我们隐式行的模式。逻辑如下

  • 如果我们有一行,它将使用第一个值进行大小调整。
  • 如果我们有两行,第一行使用第一个值,第二行使用第二个值。
  • 如果我们有三行,将使用这三个值。
  • 如果我们有四行(这里来了有趣的部分),我们将前三个行使用这三个值,然后对第四行再次重复使用第一个值。这就是为什么它是一种重复使用的模式,用于调整所有隐式行的大小。
  • 如果我们有 100 行,它们将以三行一组的方式调整大小,形成2fr 2fr 1fr 2fr 2fr 1fr 2fr 2fr 1fr 等。

与定义行数及其大小的grid-template-rows 不同,grid-auto-rows 只调整可能在途中创建的行的大小。

如果我们回到我们的示例,逻辑是当创建两行时具有相同的大小(我们将使用2fr 2fr),但是如果创建了第三行,我们将使其稍微小一些。

网格模式

对于最后一个,我们将讨论模式。您可能已经看到过那些两列布局,其中一列比另一列宽,并且每一行交替放置这些列。

如果不确切知道我们正在处理多少内容,这种布局可能很难实现,但 CSS Grid 的自动放置功能使其变得相对容易。

看一下代码。它可能看起来很复杂,但让我们将其分解,因为它最终变得非常简单。

首先要确定模式。问问自己:“模式应该在多少个元素之后重复?”在本例中,每四个元素后重复一次。因此,让我们先看看只使用四个元素的情况。

现在,让我们定义网格并使用:nth-child 选择器在元素之间交替设置通用模式。

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows equal to 100px */
}
.grid :nth-child(4n + 1) { /* ?? */ }
.grid :nth-child(4n + 2) { /* ?? */ }
.grid :nth-child(4n + 3) { /* ?? */ }
.grid :nth-child(4n + 4) { /* ?? */ }

我们说我们的模式每四个元素重复一次,所以我们将逻辑地使用4n + x,其中x 的范围从 1 到 4。用这种方式解释模式更容易一些。

4(0) + 1 = 1 = 1st element /* we start with n = 0 */
4(0) + 2 = 2 = 2nd element
4(0) + 3 = 3 = 3rd element
4(0) + 4 = 4 = 4th element
4(1) + 1 = 5 = 5th element /* our pattern repeat here at n = 1 */
4(1) + 2 = 6 = 6th element
4(1) + 3 = 7 = 7th element
4(1) + 4 = 8 = 8th element
4(2) + 1 = 9 = 9th element /* our pattern repeat again here at n = 2 */
etc.

完美,对吧?我们有四个元素,并在第五个元素、第九个元素等元素上重复模式。

这些:nth-child 选择器可能很棘手!Chris 有一个非常有帮助的关于其工作原理的解释,包括创建不同模式的食谱

现在我们配置每个元素,以便

  1. 第一个元素需要占用两列并从第一列开始(grid-column: 1/span 2)。
  2. 第二个元素放置在第三列(grid-column-start: 3)。
  3. 第三个元素放置在第一列:(grid-column-start: 1)。
  4. 第四个元素占用两列并从第二列开始:(grid-column: 2/span 2)。

以下是 CSS 中的代码

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: 1/span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 3) { grid-column-start: 1; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

我们可以到此为止……但我们可以做得更好!具体来说,我们可以删除一些声明并依靠网格的自动放置功能来完成工作。这是最难理解的部分,需要大量的练习才能识别出哪些可以删除。

我们可以做的第一件事是更新grid-column: 1 /span 2 并仅使用grid-column: span 2,因为默认情况下,浏览器会将第一个项目放置在第一列。我们也可以删除这个

.grid :nth-child(4n + 3) { grid-column-start: 1; }

通过放置第一、第二和第四个项目,网格会自动将第三个项目放置在正确的位置。这意味着我们剩下的是这个

.grid {
  display: grid;
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
  grid-auto-columns: 1fr; /* all the columns are equal */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

但是,我们可以做得更好!我们还可以删除这个

.grid :nth-child(4n + 2) { grid-column-start: 3; }

为什么?如果我们将第四个元素放置在第二列,同时允许它占据两列,我们将强制网格创建一个第三个隐式列,从而使我们总共有三个列,而无需明确告诉它。第四个元素不能进入第一行,因为第一个项目也占据了两列,因此它会流到下一行。此配置会在第一行留下一个空列,在第二行留下一个空列。

我想你知道故事的结局。浏览器会自动将第二和第三个项目放置在这些空位上。因此,我们的代码变得更加简单

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

只需五个声明即可创建一个非常酷且非常灵活的模式。优化部分可能很棘手,但你习惯了它,并通过实践获得了一些技巧。

既然我们知道列数,为什么不使用grid-template-columns 来定义显式列呢?

我们可以这样做!以下是它的代码

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1),
.grid :nth-child(4n + 4) {
  grid-column: span 2;
}

如您所见,代码肯定更直观。我们定义了三个显式网格列,并告诉浏览器第一和第四个元素需要占用两列。我强烈推荐这种方法!但本文的目的是探索 CSS Grid 隐式和自动放置功能带来的新想法和技巧。

显式方法更直观,而隐式网格则要求您——恕我直言——填补 CSS 在幕后执行额外工作的空白。最后,我相信深入理解隐式网格将帮助您更好地理解 CSS Grid 算法。毕竟,我们不是来研究显而易见的事物的——我们是在探索广阔的领域!

让我们再试一个模式,这次更快一点

我们的模式每六个元素重复一次。第三和第四个元素每个需要占据两行。如果我们放置第三和第四个元素,似乎我们不需要触碰其他元素,所以让我们尝试以下操作

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 3) {
  grid-area: span 2/2; /* grid-row-start: span 2 && grid-column-start: 2 */
}
.grid :nth-child(6n + 4) {
  grid-area: span 2/1; /* grid-row-start: span 2 && grid-column-start: 1 */
}

嗯,不行。我们需要将第二个元素放置在第一列。否则,网格将自动将其放置在第二列。

.grid :nth-child(6n + 2) {
  grid-column: 1; /* grid-column-start: 1 */
}

好些了,但仍然有更多工作要做,我们需要将第三个元素向上移动。尝试将其放置在第一行很有诱惑力,方法如下

.grid :nth-child(6n + 3) {
  grid-area: 1/2/span 2; 
    /* Equivalent to:
       grid-row-start: 1;
       grid-row-end: span 2;
       grid-column-start: 2 
     */
}

但这不起作用,因为它强制所有6n + 3 元素放置在同一区域,这会导致布局混乱。真正的解决方案是保留第三个元素的初始定义,并添加grid-auto-flow: dense 来填补空白。来自 MDN

“[密集型]” 排列算法尝试在网格中更早地填充空隙,如果稍后出现较小的项目。这可能会导致项目在排列时出现顺序错误,因为这样做会填充较大型项目留下的空隙。如果省略此属性,则使用“[稀疏型]”算法,其中放置算法在放置项目时仅沿网格“向前”移动,从不回溯以填充空隙。这确保了所有自动放置的项目都“按顺序”出现,即使这会留下可能被后续项目填充的空隙。

我知道这个特性不太直观,但在遇到布局问题时千万不要忘记它。在徒劳地尝试不同的配置之前,请添加它,因为它可能会在无需额外操作的情况下修复您的布局。

为什么不默认总是添加此属性呢?

我不建议这样做,因为在某些情况下,我们不希望出现这种行为。请注意 MDN 中的解释如何提到它会导致项目“乱序”流动以填充较大型项目留下的空隙。视觉顺序通常与源顺序一样重要,尤其是在可访问的界面中,而grid-auto-flow: dense有时会导致视觉顺序和源顺序不匹配。

我们的最终代码如下

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: dense;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 2) { grid-column: 1; }
.grid :nth-child(6n + 3) { grid-area: span 2/2; }
.grid :nth-child(6n + 4) { grid-row: span 2; }

另一个?让我们开始吧!

对于这个,我不会说得太多,而是向您展示我使用的代码示例。尝试看看您是否理解了我如何得到这段代码。

黑色项目是隐式放置在网格中的。需要注意的是,我们可以通过多种方式获得相同的布局,而不仅仅是我得到的方式。您能否也找出这些方法?使用grid-template-columns怎么样?在评论区分享您的作品。

我将为您留下最后一个模式。

我确实有一个解决方案,但现在轮到您练习了。运用我们学到的所有知识,尝试自己编写这段代码,然后将其与我的解决方案进行比较。如果您的代码比较冗长,请不要担心——最重要的是找到一个可行的解决方案。

想要更多?

在结束之前,我想分享一些与 CSS Grid 相关的 Stack Overflow 问题,我在其中用我们在这里一起介绍的许多技术给出了答案。这是一个很好的列表,它展示了这些东西在多少实际用例和现实情况中派上用场。

总结

CSS Grid 已经存在多年了,但仍然有很多鲜为人知和使用的技巧没有得到广泛讨论。隐式网格和自动放置功能就是其中两个!

是的,这可能会变得很复杂!我花了很长时间才理解隐式网格背后的逻辑,并且我仍然在与自动放置作斗争。如果您想花更多时间来理解显式和隐式网格,这里有一些额外的解释和示例值得查看。

同样,您可能希望在 CSS-Tricks 年鉴中阅读有关grid-auto-columns的信息,因为 Mojtaba Seyedi 详细介绍了它,并包含非常有用的视觉效果来帮助解释其行为。

就像我们在开始时所说的,我们在这里介绍的方法并非旨在取代您已经知道的构建网格的常用方法。我只是在探索在某些情况下可能有所帮助的不同方法。